Merge remote-tracking branch 'origin/fnux-stable'
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
This commit is contained in:
		
				commit
				
					
						a50095f873
					
				
			
		
					 49 changed files with 1340 additions and 116 deletions
				
			
		| 
						 | 
				
			
			@ -14,4 +14,7 @@ LDAP_ADMIN_DN=""
 | 
			
		|||
LDAP_ADMIN_PASSWORD=""
 | 
			
		||||
LDAP_SERVER_URI = ""
 | 
			
		||||
 | 
			
		||||
# Stripe (Credit Card payments)
 | 
			
		||||
STRIPE_API_key=""
 | 
			
		||||
 | 
			
		||||
SECRET_KEY="dx$iqt=lc&yrp^!z5$ay^%g5lhx1y3bcu=jg(jx0yj0ogkfqvf"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -62,7 +62,9 @@ INSTALLED_APPS = [
 | 
			
		|||
    'rest_framework',
 | 
			
		||||
    'uncloud_pay',
 | 
			
		||||
    'uncloud_auth',
 | 
			
		||||
    'uncloud_storage',
 | 
			
		||||
    'uncloud_vm',
 | 
			
		||||
    'ungleich_service',
 | 
			
		||||
    'opennebula'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -174,3 +176,8 @@ USE_TZ = True
 | 
			
		|||
STATIC_URL = '/static/'
 | 
			
		||||
 | 
			
		||||
stripe.api_key = uncloud.secrets.STRIPE_KEY
 | 
			
		||||
 | 
			
		||||
############
 | 
			
		||||
# Stripe
 | 
			
		||||
 | 
			
		||||
STRIPE_API_KEY = uncloud.secrets.STRIPE_API_KEY
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,11 +20,12 @@ from rest_framework import routers
 | 
			
		|||
 | 
			
		||||
from uncloud_vm import views as vmviews
 | 
			
		||||
from uncloud_pay import views as payviews
 | 
			
		||||
from ungleich_service import views as serviceviews
 | 
			
		||||
from opennebula import views as oneviews
 | 
			
		||||
 | 
			
		||||
router = routers.DefaultRouter()
 | 
			
		||||
 | 
			
		||||
# user / regular urls
 | 
			
		||||
# VM
 | 
			
		||||
router.register(r'vm/snapshot', vmviews.VMSnapshotProductViewSet, basename='vmsnapshotproduct')
 | 
			
		||||
router.register(r'vm/disk', vmviews.VMDiskProductViewSet, basename='vmdiskproduct')
 | 
			
		||||
router.register(r'vm/image/mine', vmviews.VMDiskImageProductMineViewSet, basename='vmdiskimagemineproduct')
 | 
			
		||||
| 
						 | 
				
			
			@ -38,6 +39,7 @@ router.register(r'vm/image/public', vmviews.VMDiskImageProductPublicViewSet, bas
 | 
			
		|||
 | 
			
		||||
router.register(r'vm/vm', vmviews.VMProductViewSet, basename='vmproduct')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# TBD
 | 
			
		||||
#router.register(r'vm/disk', vmviews.VMDiskProductViewSet, basename='vmdiskproduct')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -49,11 +51,20 @@ router.register(r'vm/vm', vmviews.VMProductViewSet, basename='vmproduct')
 | 
			
		|||
# allow vm creation from own images
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Services
 | 
			
		||||
router.register(r'service/matrix', serviceviews.MatrixServiceProductViewSet, basename='matrixserviceproduct')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Pay
 | 
			
		||||
router.register(r'user', payviews.UserViewSet, basename='user')
 | 
			
		||||
router.register(r'payment-method', payviews.PaymentMethodViewSet, basename='payment-method')
 | 
			
		||||
router.register(r'bill', payviews.BillViewSet, basename='bill')
 | 
			
		||||
router.register(r'order', payviews.OrderViewSet, basename='order')
 | 
			
		||||
router.register(r'payment', payviews.PaymentViewSet, basename='payment')
 | 
			
		||||
router.register(r'payment-method', payviews.PaymentMethodViewSet, basename='payment-methods')
 | 
			
		||||
 | 
			
		||||
# VMs
 | 
			
		||||
router.register(r'vm/vm', vmviews.VMProductViewSet, basename='vm')
 | 
			
		||||
 | 
			
		||||
# admin/staff urls
 | 
			
		||||
router.register(r'admin/bill', payviews.AdminBillViewSet, basename='admin/bill')
 | 
			
		||||
| 
						 | 
				
			
			@ -61,7 +72,7 @@ router.register(r'admin/payment', payviews.AdminPaymentViewSet, basename='admin/
 | 
			
		|||
router.register(r'admin/order', payviews.AdminOrderViewSet, basename='admin/order')
 | 
			
		||||
router.register(r'admin/vmhost', vmviews.VMHostViewSet)
 | 
			
		||||
router.register(r'admin/opennebula', oneviews.VMViewSet, basename='opennebula')
 | 
			
		||||
router.register(r'admin/opennebula_raw', oneviews.RawVMViewSet)
 | 
			
		||||
router.register(r'admin/opennebula_raw', oneviews.RawVMViewSet, basename='opennebula_raw')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										96
									
								
								uncloud/uncloud_pay/helpers.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								uncloud/uncloud_pay/helpers.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,96 @@
 | 
			
		|||
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)
 | 
			
		||||
 | 
			
		||||
def end_of_month(year, month):
 | 
			
		||||
    (_, days) = monthrange(year, month)
 | 
			
		||||
    tz = timezone.get_current_timezone()
 | 
			
		||||
    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,
 | 
			
		||||
                   GenericViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    A customer-facing viewset that provides default `create()`, `retrieve()`
 | 
			
		||||
    and `list()`.
 | 
			
		||||
    """
 | 
			
		||||
    pass
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
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 datetime import timedelta
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = 'Generate bills and charge customers if necessary.'
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        users = User.objects.all()
 | 
			
		||||
        print("Processing {} users.".format(users.count()))
 | 
			
		||||
        for user in users:
 | 
			
		||||
            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)
 | 
			
		||||
                if payment_method != None:
 | 
			
		||||
                    amount_to_be_charged = abs(balance)
 | 
			
		||||
                    charge_ok = payment_method.charge(amount_to_be_charged)
 | 
			
		||||
                    if not charge_ok:
 | 
			
		||||
                        print("ERR: charging {} with method {} failed"
 | 
			
		||||
                                .format(user.username, payment_method.uuid)
 | 
			
		||||
                                )
 | 
			
		||||
                else:
 | 
			
		||||
                    print("ERR: no payment method registered for {}".format(user.username))
 | 
			
		||||
        print("=> Done.")
 | 
			
		||||
							
								
								
									
										38
									
								
								uncloud/uncloud_pay/management/commands/generate-bills.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								uncloud/uncloud_pay/management/commands/generate-bills.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
import logging
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from uncloud_auth.models import User
 | 
			
		||||
from uncloud_pay.models import Order, Bill
 | 
			
		||||
from django.core.exceptions import ObjectDoesNotExist
 | 
			
		||||
 | 
			
		||||
from datetime import timedelta, date
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from uncloud_pay.helpers import generate_bills_for
 | 
			
		||||
 | 
			
		||||
BILL_PAYMENT_DELAY=timedelta(days=10)
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = 'Generate bills and charge customers if necessary.'
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    # TODO: use logger.*
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        # Iterate over all 'active' users.
 | 
			
		||||
        # TODO: filter out inactive users.
 | 
			
		||||
        users = User.objects.all()
 | 
			
		||||
        print("Processing {} users.".format(users.count()))
 | 
			
		||||
 | 
			
		||||
        for user in users:
 | 
			
		||||
            now = timezone.now()
 | 
			
		||||
            generate_bills_for(
 | 
			
		||||
                    year=now.year,
 | 
			
		||||
                    month=now.month,
 | 
			
		||||
                    user=user,
 | 
			
		||||
                    allowed_delay=BILL_PAYMENT_DELAY)
 | 
			
		||||
 | 
			
		||||
        # We're done for this round :-)
 | 
			
		||||
        print("=> Done.")
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
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 datetime import timedelta
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = 'Generate bills and charge customers if necessary.'
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        users = User.objects.all()
 | 
			
		||||
        print("Processing {} users.".format(users.count()))
 | 
			
		||||
        for user in users:
 | 
			
		||||
            balance = get_balance_for(user)
 | 
			
		||||
            if balance < 0:
 | 
			
		||||
                print("User {} has negative balance ({}), checking for overdue bills."
 | 
			
		||||
                        .format(user.username, balance))
 | 
			
		||||
 | 
			
		||||
                # 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.")
 | 
			
		||||
							
								
								
									
										32
									
								
								uncloud/uncloud_pay/migrations/0002_auto_20200227_1404.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								uncloud/uncloud_pay/migrations/0002_auto_20200227_1404.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-27 14:04
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
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', '0001_initial'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='payment',
 | 
			
		||||
            name='source',
 | 
			
		||||
            field=models.CharField(choices=[('wire', 'Wire Transfer'), ('stripe', 'Stripe'), ('voucher', 'Voucher'), ('referral', 'Referral'), ('unknown', 'Unknown')], default='unknown', max_length=256),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='PaymentMethod',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
 | 
			
		||||
                ('source', models.CharField(choices=[('stripe', 'Stripe'), ('unknown', 'Unknown')], default='stripe', max_length=256)),
 | 
			
		||||
                ('description', models.TextField()),
 | 
			
		||||
                ('default', models.BooleanField()),
 | 
			
		||||
                ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										28
									
								
								uncloud/uncloud_pay/migrations/0003_auto_20200227_1414.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								uncloud/uncloud_pay/migrations/0003_auto_20200227_1414.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-27 14:14
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
        ('uncloud_pay', '0002_auto_20200227_1404'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='paymentmethod',
 | 
			
		||||
            name='primary',
 | 
			
		||||
            field=models.BooleanField(default=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterUniqueTogether(
 | 
			
		||||
            name='paymentmethod',
 | 
			
		||||
            unique_together={('owner', 'primary')},
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='paymentmethod',
 | 
			
		||||
            name='default',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										31
									
								
								uncloud/uncloud_pay/migrations/0004_auto_20200227_1532.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								uncloud/uncloud_pay/migrations/0004_auto_20200227_1532.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-27 15:32
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
        ('uncloud_pay', '0003_auto_20200227_1414'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='bill',
 | 
			
		||||
            name='owner',
 | 
			
		||||
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='order',
 | 
			
		||||
            name='recurring_period',
 | 
			
		||||
            field=models.CharField(choices=[('ONCE', 'Onetime'), ('YEAR', 'Per Year'), ('MONTH', 'Per Month'), ('MINUTE', 'Per Minute'), ('DAY', 'Per Day'), ('HOUR', 'Per Hour'), ('SECOND', 'Per Second')], default='MONTH', max_length=32),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='payment',
 | 
			
		||||
            name='owner',
 | 
			
		||||
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										42
									
								
								uncloud/uncloud_pay/migrations/0005_auto_20200228_0737.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								uncloud/uncloud_pay/migrations/0005_auto_20200228_0737.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-28 07:37
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_pay', '0004_auto_20200227_1532'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='bill',
 | 
			
		||||
            name='id',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='bill',
 | 
			
		||||
            name='paid',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='bill',
 | 
			
		||||
            name='uuid',
 | 
			
		||||
            field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='bill',
 | 
			
		||||
            name='creation_date',
 | 
			
		||||
            field=models.DateTimeField(auto_now_add=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='order',
 | 
			
		||||
            name='creation_date',
 | 
			
		||||
            field=models.DateTimeField(auto_now_add=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='order',
 | 
			
		||||
            name='starting_date',
 | 
			
		||||
            field=models.DateTimeField(auto_now_add=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										18
									
								
								uncloud/uncloud_pay/migrations/0006_auto_20200228_0741.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								uncloud/uncloud_pay/migrations/0006_auto_20200228_0741.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-28 07:41
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_pay', '0005_auto_20200228_0737'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='order',
 | 
			
		||||
            name='bill',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, editable=False, to='uncloud_pay.Bill'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										17
									
								
								uncloud/uncloud_pay/migrations/0007_remove_order_bill.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								uncloud/uncloud_pay/migrations/0007_remove_order_bill.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-28 07:44
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_pay', '0006_auto_20200228_0741'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='order',
 | 
			
		||||
            name='bill',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										18
									
								
								uncloud/uncloud_pay/migrations/0008_order_bill.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								uncloud/uncloud_pay/migrations/0008_order_bill.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-28 07:44
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_pay', '0007_remove_order_bill'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='order',
 | 
			
		||||
            name='bill',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, editable=False, to='uncloud_pay.Bill'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										29
									
								
								uncloud/uncloud_pay/migrations/0009_auto_20200228_0825.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								uncloud/uncloud_pay/migrations/0009_auto_20200228_0825.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-28 08:25
 | 
			
		||||
 | 
			
		||||
import django.core.validators
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_pay', '0008_order_bill'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='order',
 | 
			
		||||
            name='one_time_price',
 | 
			
		||||
            field=models.DecimalField(decimal_places=2, editable=False, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='order',
 | 
			
		||||
            name='recurring_price',
 | 
			
		||||
            field=models.DecimalField(decimal_places=2, editable=False, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='payment',
 | 
			
		||||
            name='timestamp',
 | 
			
		||||
            field=models.DateTimeField(auto_now_add=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										14
									
								
								uncloud/uncloud_pay/migrations/0010_merge_20200228_1303.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								uncloud/uncloud_pay/migrations/0010_merge_20200228_1303.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-28 13:03
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_pay', '0002_auto_20200227_1230'),
 | 
			
		||||
        ('uncloud_pay', '0009_auto_20200228_0825'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										21
									
								
								uncloud/uncloud_pay/migrations/0011_auto_20200229_1459.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								uncloud/uncloud_pay/migrations/0011_auto_20200229_1459.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-29 14:59
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_pay', '0010_merge_20200228_1303'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='order',
 | 
			
		||||
            name='one_time_price',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='order',
 | 
			
		||||
            name='recurring_price',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										25
									
								
								uncloud/uncloud_pay/migrations/0012_orderrecord.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								uncloud/uncloud_pay/migrations/0012_orderrecord.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-03-01 16:04
 | 
			
		||||
 | 
			
		||||
import django.core.validators
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_pay', '0011_auto_20200229_1459'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='OrderRecord',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('setup_fee', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])),
 | 
			
		||||
                ('recurring_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])),
 | 
			
		||||
                ('description', models.TextField()),
 | 
			
		||||
                ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.Order')),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-03-02 20:14
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_pay', '0012_orderrecord'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='paymentmethod',
 | 
			
		||||
            name='stripe_card_id',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=32, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -1,81 +1,37 @@
 | 
			
		|||
from django.db import models
 | 
			
		||||
from functools import reduce
 | 
			
		||||
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 math import ceil
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from calendar import monthrange
 | 
			
		||||
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
# Define DecimalField properties, used to represent amounts of money.
 | 
			
		||||
AMOUNT_MAX_DIGITS=10
 | 
			
		||||
AMOUNT_DECIMALS=2
 | 
			
		||||
 | 
			
		||||
class Bill(models.Model):
 | 
			
		||||
    owner = models.ForeignKey(get_user_model(),
 | 
			
		||||
            on_delete=models.CASCADE,
 | 
			
		||||
            editable=False)
 | 
			
		||||
 | 
			
		||||
    creation_date = models.DateTimeField()
 | 
			
		||||
    starting_date = models.DateTimeField()
 | 
			
		||||
    ending_date = models.DateTimeField()
 | 
			
		||||
    due_date = models.DateField()
 | 
			
		||||
 | 
			
		||||
    paid = models.BooleanField(default=False)
 | 
			
		||||
    valid = models.BooleanField(default=True)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def amount(self):
 | 
			
		||||
        # iterate over all related orders
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Order(models.Model):
 | 
			
		||||
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
 | 
			
		||||
    owner = models.ForeignKey(get_user_model(),
 | 
			
		||||
                              on_delete=models.CASCADE,
 | 
			
		||||
                              editable=False)
 | 
			
		||||
 | 
			
		||||
    creation_date = models.DateTimeField()
 | 
			
		||||
    starting_date = models.DateTimeField()
 | 
			
		||||
    ending_date = models.DateTimeField(blank=True,
 | 
			
		||||
                                       null=True)
 | 
			
		||||
 | 
			
		||||
    bill = models.ManyToManyField(Bill,
 | 
			
		||||
                                  editable=False,
 | 
			
		||||
                                  blank=True,
 | 
			
		||||
                                  null=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    recurring_price = models.FloatField(editable=False)
 | 
			
		||||
    one_time_price = models.FloatField(editable=False)
 | 
			
		||||
 | 
			
		||||
    recurring_period = models.CharField(max_length=32,
 | 
			
		||||
                              choices = (
 | 
			
		||||
                                  ('onetime', 'Onetime'),
 | 
			
		||||
                                  ('per_year', 'Per Year'),
 | 
			
		||||
                                  ('per_month', 'Per Month'),
 | 
			
		||||
                                  ('per_week', 'Per Week'),
 | 
			
		||||
                                  ('per_day', 'Per Day'),
 | 
			
		||||
                                  ('per_hour', 'Per Hour'),
 | 
			
		||||
                                  ('per_minute', 'Per Minute'),
 | 
			
		||||
                                  ('per_second', 'Per Second'),
 | 
			
		||||
                              ),
 | 
			
		||||
                              default='onetime'
 | 
			
		||||
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # def amount(self):
 | 
			
		||||
    #     amount = recurring_price
 | 
			
		||||
    #     if recurring and first_month:
 | 
			
		||||
    #         amount += one_time_price
 | 
			
		||||
 | 
			
		||||
    #     return amount # you get the picture
 | 
			
		||||
 | 
			
		||||
# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
 | 
			
		||||
class RecurringPeriod(models.TextChoices):
 | 
			
		||||
    ONE_TIME   = 'ONCE', _('Onetime')
 | 
			
		||||
    PER_YEAR   = 'YEAR', _('Per Year')
 | 
			
		||||
    PER_MONTH  = 'MONTH', _('Per Month')
 | 
			
		||||
    PER_MINUTE = 'MINUTE', _('Per Minute')
 | 
			
		||||
    PER_DAY    = 'DAY', _('Per Day')
 | 
			
		||||
    PER_HOUR   = 'HOUR', _('Per Hour')
 | 
			
		||||
    PER_SECOND = 'SECOND', _('Per Second')
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
# Payments and Payment Methods.
 | 
			
		||||
 | 
			
		||||
class Payment(models.Model):
 | 
			
		||||
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
 | 
			
		||||
 | 
			
		||||
    owner = models.ForeignKey(get_user_model(),
 | 
			
		||||
            on_delete=models.CASCADE,
 | 
			
		||||
            editable=False)
 | 
			
		||||
            on_delete=models.CASCADE)
 | 
			
		||||
 | 
			
		||||
    amount = models.DecimalField(
 | 
			
		||||
            default=0.0,
 | 
			
		||||
| 
						 | 
				
			
			@ -92,11 +48,226 @@ class Payment(models.Model):
 | 
			
		|||
                                  ('unknown', 'Unknown')
 | 
			
		||||
                              ),
 | 
			
		||||
                              default='unknown')
 | 
			
		||||
    timestamp = models.DateTimeField(editable=False)
 | 
			
		||||
    timestamp = models.DateTimeField(editable=False, auto_now_add=True)
 | 
			
		||||
 | 
			
		||||
class PaymentMethod(models.Model):
 | 
			
		||||
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
 | 
			
		||||
    owner = models.ForeignKey(get_user_model(),
 | 
			
		||||
            on_delete=models.CASCADE,
 | 
			
		||||
            editable=False)
 | 
			
		||||
    source = models.CharField(max_length=256,
 | 
			
		||||
            choices = (
 | 
			
		||||
                ('stripe', 'Stripe'),
 | 
			
		||||
                ('unknown', 'Unknown'),
 | 
			
		||||
                ),
 | 
			
		||||
            default='stripe')
 | 
			
		||||
    description = models.TextField()
 | 
			
		||||
    primary = models.BooleanField(default=True)
 | 
			
		||||
 | 
			
		||||
    # Only used for "Stripe" source
 | 
			
		||||
    stripe_card_id = models.CharField(max_length=32, blank=True, null=True)
 | 
			
		||||
 | 
			
		||||
    def charge(self, amount):
 | 
			
		||||
        if amount > 0: # Make sure we don't charge negative amount by errors...
 | 
			
		||||
            if self.source == 'stripe':
 | 
			
		||||
                # TODO: wire to stripe, see meooow-payv1/strip_utils.py
 | 
			
		||||
                payment = Payment(owner=self.owner, source=self.source, amount=amount)
 | 
			
		||||
                payment.save() # TODO: Check return status
 | 
			
		||||
 | 
			
		||||
                return True
 | 
			
		||||
            else:
 | 
			
		||||
                # We do not handle that source yet.
 | 
			
		||||
                return False
 | 
			
		||||
        else:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        unique_together = [['owner', 'primary']]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
# Bills & Payments.
 | 
			
		||||
 | 
			
		||||
class Bill(models.Model):
 | 
			
		||||
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
 | 
			
		||||
    owner = models.ForeignKey(get_user_model(),
 | 
			
		||||
            on_delete=models.CASCADE)
 | 
			
		||||
 | 
			
		||||
    creation_date = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    starting_date = models.DateTimeField()
 | 
			
		||||
    ending_date = models.DateTimeField()
 | 
			
		||||
    due_date = models.DateField()
 | 
			
		||||
 | 
			
		||||
    valid = models.BooleanField(default=True)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def records(self):
 | 
			
		||||
        bill_records = []
 | 
			
		||||
        orders = Order.objects.filter(bill=self)
 | 
			
		||||
        for order in orders:
 | 
			
		||||
            for order_record in order.records:
 | 
			
		||||
                bill_record = BillRecord(self, order_record)
 | 
			
		||||
                bill_records.append(bill_record)
 | 
			
		||||
 | 
			
		||||
        return bill_records
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def total(self):
 | 
			
		||||
        return reduce(lambda acc, record: acc + record.amount(), self.records, 0)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def final(self):
 | 
			
		||||
        # A bill is final when its ending date is passed.
 | 
			
		||||
        return self.ending_date < timezone.now()
 | 
			
		||||
 | 
			
		||||
class BillRecord():
 | 
			
		||||
    """
 | 
			
		||||
    Entry of a bill, dynamically generated from order records.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, bill, order_record):
 | 
			
		||||
        self.bill = bill
 | 
			
		||||
        self.order = order_record.order
 | 
			
		||||
        self.setup_fee = order_record.setup_fee
 | 
			
		||||
        self.recurring_price = order_record.recurring_price
 | 
			
		||||
        self.recurring_period = order_record.recurring_period
 | 
			
		||||
        self.description = order_record.description
 | 
			
		||||
 | 
			
		||||
    def amount(self):
 | 
			
		||||
        # Compute billing delta.
 | 
			
		||||
        billed_until = self.bill.ending_date
 | 
			
		||||
        if self.order.ending_date != None and self.order.ending_date < self.order.ending_date:
 | 
			
		||||
            billed_until = self.order.ending_date
 | 
			
		||||
 | 
			
		||||
        billed_from = self.bill.starting_date
 | 
			
		||||
        if self.order.starting_date > self.bill.starting_date:
 | 
			
		||||
            billed_from = self.order.starting_date
 | 
			
		||||
 | 
			
		||||
        if billed_from > billed_until:
 | 
			
		||||
            # TODO: think about and check edges cases. This should not be
 | 
			
		||||
            # possible.
 | 
			
		||||
            raise Exception('Impossible billing delta!')
 | 
			
		||||
 | 
			
		||||
        billed_delta = billed_until - billed_from
 | 
			
		||||
 | 
			
		||||
        # TODO: refactor this thing?
 | 
			
		||||
        # TODO: weekly
 | 
			
		||||
        # TODO: yearly
 | 
			
		||||
        if self.recurring_period == RecurringPeriod.PER_MONTH:
 | 
			
		||||
            days = ceil(billed_delta / timedelta(days=1))
 | 
			
		||||
 | 
			
		||||
            # XXX: we assume monthly bills for now.
 | 
			
		||||
            if (self.bill.starting_date.year != self.bill.starting_date.year or
 | 
			
		||||
                self.bill.starting_date.month != self.bill.ending_date.month):
 | 
			
		||||
                    raise Exception('Bill {} covers more than one month. Cannot bill PER_MONTH.'.
 | 
			
		||||
                        format(self.bill.uuid))
 | 
			
		||||
 | 
			
		||||
            # XXX: minumal length of monthly order is to be enforced somewhere else.
 | 
			
		||||
            (_, days_in_month) = monthrange(
 | 
			
		||||
                    self.bill.starting_date.year,
 | 
			
		||||
                    self.bill.starting_date.month)
 | 
			
		||||
            adjusted_recurring_price = self.recurring_price / days_in_month
 | 
			
		||||
            recurring_price = adjusted_recurring_price * days
 | 
			
		||||
 | 
			
		||||
            return self.recurring_price # TODO
 | 
			
		||||
        elif self.recurring_period == RecurringPeriod.PER_DAY:
 | 
			
		||||
            days = ceil(billed_delta / timedelta(days=1))
 | 
			
		||||
            return self.recurring_price * days
 | 
			
		||||
        elif self.recurring_period == RecurringPeriod.PER_HOUR:
 | 
			
		||||
            hours = ceil(billed_delta / timedelta(hours=1))
 | 
			
		||||
            return self.recurring_price * hours
 | 
			
		||||
        elif self.recurring_period == RecurringPeriod.PER_SECOND:
 | 
			
		||||
            seconds = ceil(billed_delta / timedelta(seconds=1))
 | 
			
		||||
            return self.recurring_price * seconds
 | 
			
		||||
        else:
 | 
			
		||||
            raise Exception('Unsupported recurring period: {}.'.
 | 
			
		||||
                    format(record.recurring_period))
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
# Orders.
 | 
			
		||||
 | 
			
		||||
# /!\ BIG FAT WARNING /!\ #
 | 
			
		||||
#
 | 
			
		||||
# Order are assumed IMMUTABLE and used as SOURCE OF TRUST for generating
 | 
			
		||||
# bills. Do **NOT** mutate then!
 | 
			
		||||
#
 | 
			
		||||
# Why? We need to store the state somewhere since product are mutable (e.g.
 | 
			
		||||
# adding RAM to VM, changing price of 1GB of RAM, ...). An alternative could
 | 
			
		||||
# have been to only store the state in bills but would have been more
 | 
			
		||||
# confusing: the order is a 'contract' with the customer, were both parts
 | 
			
		||||
# agree on deal => That's what we want to keep archived.
 | 
			
		||||
#
 | 
			
		||||
# /!\ BIG FAT WARNING /!\ #
 | 
			
		||||
 | 
			
		||||
class Order(models.Model):
 | 
			
		||||
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
 | 
			
		||||
    owner = models.ForeignKey(get_user_model(),
 | 
			
		||||
                              on_delete=models.CASCADE,
 | 
			
		||||
                              editable=False)
 | 
			
		||||
 | 
			
		||||
    # TODO: enforce ending_date - starting_date to be larger than recurring_period.
 | 
			
		||||
    creation_date = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    starting_date = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    ending_date = models.DateTimeField(blank=True,
 | 
			
		||||
                                       null=True)
 | 
			
		||||
 | 
			
		||||
    bill = models.ManyToManyField(Bill,
 | 
			
		||||
                                  editable=False,
 | 
			
		||||
                                  blank=True)
 | 
			
		||||
 | 
			
		||||
    recurring_period = models.CharField(max_length=32,
 | 
			
		||||
                              choices = RecurringPeriod.choices,
 | 
			
		||||
                              default = RecurringPeriod.PER_MONTH)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def records(self):
 | 
			
		||||
        return OrderRecord.objects.filter(order=self)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def setup_fee(self):
 | 
			
		||||
        return reduce(lambda acc, record: acc + record.setup_fee, self.records, 0)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def recurring_price(self):
 | 
			
		||||
        return reduce(lambda acc, record: acc + record.recurring_price, self.records, 0)
 | 
			
		||||
 | 
			
		||||
    def add_record(self, setup_fee, recurring_price, description):
 | 
			
		||||
        OrderRecord.objects.create(order=self,
 | 
			
		||||
                setup_fee=setup_fee,
 | 
			
		||||
                recurring_price=recurring_price,
 | 
			
		||||
                description=description)
 | 
			
		||||
 | 
			
		||||
class OrderRecord(models.Model):
 | 
			
		||||
    order = models.ForeignKey(Order, on_delete=models.CASCADE)
 | 
			
		||||
    setup_fee = models.DecimalField(default=0.0,
 | 
			
		||||
            max_digits=AMOUNT_MAX_DIGITS,
 | 
			
		||||
            decimal_places=AMOUNT_DECIMALS,
 | 
			
		||||
            validators=[MinValueValidator(0)])
 | 
			
		||||
    recurring_price = models.DecimalField(default=0.0,
 | 
			
		||||
            max_digits=AMOUNT_MAX_DIGITS,
 | 
			
		||||
            decimal_places=AMOUNT_DECIMALS,
 | 
			
		||||
            validators=[MinValueValidator(0)])
 | 
			
		||||
 | 
			
		||||
    description = models.TextField()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def recurring_period(self):
 | 
			
		||||
        return self.order.recurring_period
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def starting_date(self):
 | 
			
		||||
        return self.order.starting_date
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ending_date(self):
 | 
			
		||||
        return self.order.ending_date
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
# Products
 | 
			
		||||
 | 
			
		||||
# Abstract (= no database representation) class used as parent for products
 | 
			
		||||
# (e.g.  uncloud_vm.models.VMProduct).
 | 
			
		||||
class Product(models.Model):
 | 
			
		||||
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
 | 
			
		||||
    owner = models.ForeignKey(get_user_model(),
 | 
			
		||||
| 
						 | 
				
			
			@ -117,7 +288,24 @@ class Product(models.Model):
 | 
			
		|||
 | 
			
		||||
    order = models.ForeignKey(Order,
 | 
			
		||||
                              on_delete=models.CASCADE,
 | 
			
		||||
                              editable=False)
 | 
			
		||||
                              editable=False,
 | 
			
		||||
                              null=True)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def recurring_price(self, recurring_period=RecurringPeriod.PER_MONTH):
 | 
			
		||||
        pass # To be implemented in child.
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def setup_fee(self):
 | 
			
		||||
        return 0
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def recurring_period(self):
 | 
			
		||||
        return self.order.recurring_period
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def allowed_recurring_periods():
 | 
			
		||||
        return RecurringPeriod.choices
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,27 +1,125 @@
 | 
			
		|||
from django.contrib.auth import get_user_model
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
from .models import Bill, Payment, Order
 | 
			
		||||
from .models import *
 | 
			
		||||
from .helpers import get_balance_for
 | 
			
		||||
 | 
			
		||||
class BillSerializer(serializers.ModelSerializer):
 | 
			
		||||
from functools import reduce
 | 
			
		||||
from uncloud_vm.serializers import VMProductSerializer
 | 
			
		||||
from uncloud_vm.models import VMProduct
 | 
			
		||||
 | 
			
		||||
import uncloud_pay.stripe as stripe
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
# Users.
 | 
			
		||||
 | 
			
		||||
class UserSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Bill
 | 
			
		||||
        fields = ['owner', 'amount', 'due_date', 'creation_date',
 | 
			
		||||
                'starting_date', 'ending_date', 'paid']
 | 
			
		||||
        model = get_user_model()
 | 
			
		||||
        fields = ['username', 'email', 'balance']
 | 
			
		||||
 | 
			
		||||
    # 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)
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
# Payments and Payment Methods.
 | 
			
		||||
 | 
			
		||||
class PaymentSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Payment
 | 
			
		||||
        fields = ['owner', 'amount', 'source', 'timestamp']
 | 
			
		||||
 | 
			
		||||
class PaymentMethodSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = PaymentMethod
 | 
			
		||||
        fields = ['source', 'description', 'primary']
 | 
			
		||||
 | 
			
		||||
class CreditCardSerializer(serializers.Serializer):
 | 
			
		||||
    number = serializers.IntegerField()
 | 
			
		||||
    exp_month = serializers.IntegerField()
 | 
			
		||||
    exp_year = serializers.IntegerField()
 | 
			
		||||
    cvc = serializers.IntegerField()
 | 
			
		||||
 | 
			
		||||
class CreatePaymentMethodSerializer(serializers.ModelSerializer):
 | 
			
		||||
    credit_card = CreditCardSerializer()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = PaymentMethod
 | 
			
		||||
        fields = ['source', 'description', 'primary', 'credit_card']
 | 
			
		||||
 | 
			
		||||
    def create(self, validated_data):
 | 
			
		||||
        credit_card = stripe.CreditCard(**validated_data.pop('credit_card'))
 | 
			
		||||
        user = self.context['request'].user
 | 
			
		||||
        customer = stripe.create_customer(user.username, user.email)
 | 
			
		||||
        # TODO check customer error
 | 
			
		||||
        customer_id = customer['response_object']['id']
 | 
			
		||||
        stripe_card = stripe.create_card(customer_id, credit_card)
 | 
			
		||||
        # TODO: check credit card error
 | 
			
		||||
        validated_data['stripe_card_id'] = stripe_card['response_object']['id']
 | 
			
		||||
class CreditCardSerializer(serializers.Serializer):
 | 
			
		||||
    number = serializers.IntegerField()
 | 
			
		||||
    exp_month = serializers.IntegerField()
 | 
			
		||||
    exp_year = serializers.IntegerField()
 | 
			
		||||
    cvc = serializers.IntegerField()
 | 
			
		||||
 | 
			
		||||
class CreatePaymentMethodSerializer(serializers.ModelSerializer):
 | 
			
		||||
    credit_card = CreditCardSerializer()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = PaymentMethod
 | 
			
		||||
        fields = ['source', 'description', 'primary', 'credit_card']
 | 
			
		||||
 | 
			
		||||
    def create(self, validated_data):
 | 
			
		||||
        credit_card = stripe.CreditCard(**validated_data.pop('credit_card'))
 | 
			
		||||
        user = self.context['request'].user
 | 
			
		||||
        customer = stripe.create_customer(user.username, user.email)
 | 
			
		||||
        # TODO check customer error
 | 
			
		||||
        customer_id = customer['response_object']['id']
 | 
			
		||||
        stripe_card = stripe.create_card(customer_id, credit_card)
 | 
			
		||||
        # TODO: check credit card error
 | 
			
		||||
        validated_data['stripe_card_id'] = stripe_card['response_object']['id']
 | 
			
		||||
        payment_method = PaymentMethod.objects.create(**validated_data)
 | 
			
		||||
        return payment_method
 | 
			
		||||
        payment_method = PaymentMethod.objects.create(**validated_data)
 | 
			
		||||
        return payment_method
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
# Bills
 | 
			
		||||
 | 
			
		||||
# TODO: remove magic numbers for decimal fields
 | 
			
		||||
class BillRecordSerializer(serializers.Serializer):
 | 
			
		||||
    order = serializers.CharField()
 | 
			
		||||
    description = serializers.CharField()
 | 
			
		||||
    recurring_period = serializers.CharField()
 | 
			
		||||
    recurring_price = serializers.DecimalField(max_digits=10, decimal_places=2)
 | 
			
		||||
    amount = serializers.DecimalField(max_digits=10, decimal_places=2)
 | 
			
		||||
 | 
			
		||||
class BillSerializer(serializers.ModelSerializer):
 | 
			
		||||
    records = BillRecordSerializer(many=True, read_only=True)
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Bill
 | 
			
		||||
        fields = ['owner', 'total', 'due_date', 'creation_date',
 | 
			
		||||
                'starting_date', 'ending_date', 'records', 'final']
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
# Orders & Products.
 | 
			
		||||
 | 
			
		||||
class OrderRecordSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = OrderRecord
 | 
			
		||||
        fields  = ['setup_fee', 'recurring_price', 'description']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OrderSerializer(serializers.ModelSerializer):
 | 
			
		||||
    records = OrderRecordSerializer(many=True, read_only=True)
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Order
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
        fields = ['uuid', 'creation_date', 'starting_date', 'ending_date',
 | 
			
		||||
                'bill', 'recurring_period', 'records', 'recurring_price', 'setup_fee']
 | 
			
		||||
 | 
			
		||||
class UserSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = get_user_model()
 | 
			
		||||
        fields = ['username', 'email']
 | 
			
		||||
 | 
			
		||||
    def get_balance(self, obj):
 | 
			
		||||
        return 666
 | 
			
		||||
class ProductSerializer(serializers.Serializer):
 | 
			
		||||
    vms = VMProductSerializer(many=True, read_only=True)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,16 @@
 | 
			
		|||
import stripe
 | 
			
		||||
import stripe.error
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
# Static stripe configuration used below.
 | 
			
		||||
CURRENCY = 'chf'
 | 
			
		||||
 | 
			
		||||
# Register stripe (secret) API key from config.
 | 
			
		||||
stripe.api_key = settings.STRIPE_API_KEY
 | 
			
		||||
 | 
			
		||||
# Helper (decorator) used to catch errors raised by stripe logic.
 | 
			
		||||
def handle_stripe_error(f):
 | 
			
		||||
    def handle_problems(*args, **kwargs):
 | 
			
		||||
        response = {
 | 
			
		||||
| 
						 | 
				
			
			@ -53,3 +64,51 @@ def handle_stripe_error(f):
 | 
			
		|||
            return response
 | 
			
		||||
 | 
			
		||||
    return handle_problems
 | 
			
		||||
 | 
			
		||||
# Convenience CC container, also used for serialization.
 | 
			
		||||
class CreditCard():
 | 
			
		||||
    number = None
 | 
			
		||||
    exp_year = None
 | 
			
		||||
    exp_month = None
 | 
			
		||||
    cvc = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self, number, exp_month, exp_year, cvc):
 | 
			
		||||
        self.number=number
 | 
			
		||||
        self.exp_year = exp_year
 | 
			
		||||
        self.exp_month = exp_month
 | 
			
		||||
        self.cvc = cvc
 | 
			
		||||
 | 
			
		||||
# Actual Stripe logic.
 | 
			
		||||
 | 
			
		||||
@handle_stripe_error
 | 
			
		||||
def create_card(customer_id, credit_card):
 | 
			
		||||
    # Test settings
 | 
			
		||||
    credit_card.number = "5555555555554444"
 | 
			
		||||
 | 
			
		||||
    return stripe.Customer.create_source(
 | 
			
		||||
            customer_id,
 | 
			
		||||
            card={
 | 
			
		||||
                'number': credit_card.number,
 | 
			
		||||
                'exp_month': credit_card.exp_month,
 | 
			
		||||
                'exp_year': credit_card.exp_year,
 | 
			
		||||
                'cvc': credit_card.cvc
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
@handle_stripe_error
 | 
			
		||||
def get_card(customer_id, card_id):
 | 
			
		||||
    return stripe.Card.retrieve_source(customer_id, card_id)
 | 
			
		||||
 | 
			
		||||
@handle_stripe_error
 | 
			
		||||
def charge_customer(amount, source):
 | 
			
		||||
    return stripe.Charge.create(
 | 
			
		||||
            amount=amount,
 | 
			
		||||
            currenty=CURRENCY,
 | 
			
		||||
            source=source)
 | 
			
		||||
 | 
			
		||||
@handle_stripe_error
 | 
			
		||||
def create_customer(name, email):
 | 
			
		||||
    return stripe.Customer.create(name=name, email=email)
 | 
			
		||||
 | 
			
		||||
@handle_stripe_error
 | 
			
		||||
def get_customer(customer_id):
 | 
			
		||||
    return stripe.Customer.retrieve(customer_id)
 | 
			
		||||
| 
						 | 
				
			
			@ -4,8 +4,10 @@ from rest_framework import viewsets, permissions, status
 | 
			
		|||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.decorators import action
 | 
			
		||||
 | 
			
		||||
from .models import Bill, Payment, Order
 | 
			
		||||
from .serializers import BillSerializer, PaymentSerializer, UserSerializer, OrderSerializer
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from .models import *
 | 
			
		||||
from .serializers import *
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
| 
						 | 
				
			
			@ -54,9 +56,39 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
 | 
			
		|||
    def get_queryset(self):
 | 
			
		||||
        return get_user_model().objects.all()
 | 
			
		||||
 | 
			
		||||
    @action(detail=True)
 | 
			
		||||
    def balance(self, request):
 | 
			
		||||
        return Response(status=status.HTTP_204_NO_CONTENT)
 | 
			
		||||
class PaymentMethodViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    permission_classes = [permissions.IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get_serializer_class(self):
 | 
			
		||||
        if self.action == 'create':
 | 
			
		||||
            return CreatePaymentMethodSerializer
 | 
			
		||||
        else:
 | 
			
		||||
            return PaymentMethodSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return PaymentMethod.objects.filter(owner=self.request.user)
 | 
			
		||||
 | 
			
		||||
    def create(self, request):
 | 
			
		||||
        serializer = self.get_serializer(data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save(owner=request.user)
 | 
			
		||||
 | 
			
		||||
        headers = self.get_success_headers(serializer.data)
 | 
			
		||||
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
 | 
			
		||||
 | 
			
		||||
    # TODO: find a way to customize serializer for actions.
 | 
			
		||||
    # drf-action-serializer module seems to do that.
 | 
			
		||||
    @action(detail=True, methods=['post'])
 | 
			
		||||
    def charge(self, request, pk=None):
 | 
			
		||||
        payment_method = self.get_object()
 | 
			
		||||
        serializer = self.get_serializer(data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        amount = serializer.data['amount']
 | 
			
		||||
        if payment_method.charge(amount):
 | 
			
		||||
            return Response({'charged', amount})
 | 
			
		||||
        else:
 | 
			
		||||
            return Response(status=status.HTTP_500_INTERNAL_ERROR)
 | 
			
		||||
 | 
			
		||||
###
 | 
			
		||||
# Admin views.
 | 
			
		||||
| 
						 | 
				
			
			@ -89,7 +121,7 @@ class AdminBillViewSet(viewsets.ModelViewSet):
 | 
			
		|||
    def create(self, request):
 | 
			
		||||
        serializer = self.get_serializer(data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save(created_at=datetime.now())
 | 
			
		||||
        serializer.save(creation_date=datetime.now())
 | 
			
		||||
 | 
			
		||||
        headers = self.get_success_headers(serializer.data)
 | 
			
		||||
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										0
									
								
								uncloud/uncloud_storage/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								uncloud/uncloud_storage/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										3
									
								
								uncloud/uncloud_storage/admin.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								uncloud/uncloud_storage/admin.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
# Register your models here.
 | 
			
		||||
							
								
								
									
										5
									
								
								uncloud/uncloud_storage/apps.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								uncloud/uncloud_storage/apps.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UncloudStorageConfig(AppConfig):
 | 
			
		||||
    name = 'uncloud_storage'
 | 
			
		||||
							
								
								
									
										0
									
								
								uncloud/uncloud_storage/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								uncloud/uncloud_storage/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										3
									
								
								uncloud/uncloud_storage/models.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								uncloud/uncloud_storage/models.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.db import models
 | 
			
		||||
 | 
			
		||||
# Create your models here.
 | 
			
		||||
							
								
								
									
										3
									
								
								uncloud/uncloud_storage/tests.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								uncloud/uncloud_storage/tests.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
# Create your tests here.
 | 
			
		||||
							
								
								
									
										3
									
								
								uncloud/uncloud_storage/views.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								uncloud/uncloud_storage/views.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.shortcuts import render
 | 
			
		||||
 | 
			
		||||
# Create your views here.
 | 
			
		||||
| 
						 | 
				
			
			@ -16,17 +16,6 @@ class Migration(migrations.Migration):
 | 
			
		|||
            model_name='vmsnapshotproduct',
 | 
			
		||||
            name='vm_uuid',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='vmproduct',
 | 
			
		||||
            name='order',
 | 
			
		||||
            field=models.ForeignKey(default=0, editable=False, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.Order'),
 | 
			
		||||
            preserve_default=False,
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='vmproduct',
 | 
			
		||||
            name='status',
 | 
			
		||||
            field=models.CharField(choices=[('pending', 'Pending'), ('being_created', 'Being created'), ('active', 'Active'), ('deleted', 'Deleted')], default='pending', max_length=256),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='vmsnapshotproduct',
 | 
			
		||||
            name='vm',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										25
									
								
								uncloud/uncloud_vm/migrations/0005_auto_20200227_1532.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								uncloud/uncloud_vm/migrations/0005_auto_20200227_1532.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-27 15:32
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_pay', '0004_auto_20200227_1532'),
 | 
			
		||||
        ('uncloud_vm', '0004_vmsnapshotproduct'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='vmproduct',
 | 
			
		||||
            name='order',
 | 
			
		||||
            field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.Order'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='vmsnapshotproduct',
 | 
			
		||||
            name='order',
 | 
			
		||||
            field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.Order'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										14
									
								
								uncloud/uncloud_vm/migrations/0006_merge_20200228_1303.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								uncloud/uncloud_vm/migrations/0006_merge_20200228_1303.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-28 13:03
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_vm', '0005_auto_20200227_1532'),
 | 
			
		||||
        ('uncloud_vm', '0005_auto_20200227_1230'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										23
									
								
								uncloud/uncloud_vm/migrations/0007_auto_20200228_1344.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								uncloud/uncloud_vm/migrations/0007_auto_20200228_1344.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-28 13:44
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_vm', '0006_merge_20200228_1303'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='vmnetworkcard',
 | 
			
		||||
            name='ip_address',
 | 
			
		||||
            field=models.GenericIPAddressField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='vmproduct',
 | 
			
		||||
            name='status',
 | 
			
		||||
            field=models.CharField(choices=[('pending', 'Pending'), ('being_created', 'Being created'), ('active', 'Active'), ('deleted', 'Deleted')], default='pending', max_length=256),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										18
									
								
								uncloud/uncloud_vm/migrations/0008_vmproduct_name.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								uncloud/uncloud_vm/migrations/0008_vmproduct_name.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-28 14:05
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_vm', '0007_auto_20200228_1344'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='vmproduct',
 | 
			
		||||
            name='name',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=32),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										18
									
								
								uncloud/uncloud_vm/migrations/0009_auto_20200228_1416.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								uncloud/uncloud_vm/migrations/0009_auto_20200228_1416.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-28 14:16
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_vm', '0008_vmproduct_name'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='vmproduct',
 | 
			
		||||
            name='name',
 | 
			
		||||
            field=models.CharField(max_length=32),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -6,7 +6,8 @@ from django.contrib.auth import get_user_model
 | 
			
		|||
# Uncomment if you override model's clean method
 | 
			
		||||
# from django.core.exceptions import ValidationError
 | 
			
		||||
 | 
			
		||||
from uncloud_pay.models import Product
 | 
			
		||||
from uncloud_pay.models import Product, RecurringPeriod
 | 
			
		||||
import uncloud_pay.models as pay_models
 | 
			
		||||
 | 
			
		||||
STATUS_CHOICES = (
 | 
			
		||||
    ('pending', 'Pending'),   # Initial state
 | 
			
		||||
| 
						 | 
				
			
			@ -45,9 +46,31 @@ class VMProduct(Product):
 | 
			
		|||
        VMHost, on_delete=models.CASCADE, editable=False, blank=True, null=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # VM-specific. The name is only intended for customers: it's a pain te
 | 
			
		||||
    # remember IDs (speaking from experience as ungleich customer)!
 | 
			
		||||
    name = models.CharField(max_length=32)
 | 
			
		||||
    cores = models.IntegerField()
 | 
			
		||||
    ram_in_gb = models.FloatField()
 | 
			
		||||
 | 
			
		||||
    def recurring_price(self, recurring_period=RecurringPeriod.PER_MONTH):
 | 
			
		||||
        # TODO: move magic numbers in variables
 | 
			
		||||
        if recurring_period == RecurringPeriod.PER_MONTH:
 | 
			
		||||
            return self.cores * 3 + self.ram_in_gb * 2
 | 
			
		||||
        elif recurring_period == RecurringPeriod.PER_HOUR:
 | 
			
		||||
            return self.cores * 4.0/(30 * 24) + self.ram_in_gb * 3.0/(30* 24)
 | 
			
		||||
        else:
 | 
			
		||||
            raise Exception('Invalid recurring period for VM Product pricing.')
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def description(self):
 | 
			
		||||
        return "Virtual machine '{}': {} core(s), {}GB memory".format(
 | 
			
		||||
                self.name, self.cores, self.ram_in_gb)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def allowed_recurring_periods():
 | 
			
		||||
        return list(filter(
 | 
			
		||||
            lambda pair: pair[0] in [RecurringPeriod.PER_MONTH, RecurringPeriod.PER_HOUR],
 | 
			
		||||
            RecurringPeriod.choices))
 | 
			
		||||
 | 
			
		||||
class VMWithOSProduct(VMProduct):
 | 
			
		||||
    pass
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,9 @@
 | 
			
		|||
from django.contrib.auth import get_user_model
 | 
			
		||||
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from .models import VMHost, VMProduct, VMSnapshotProduct, VMDiskProduct, VMDiskImageProduct
 | 
			
		||||
from uncloud_pay.models import RecurringPeriod
 | 
			
		||||
 | 
			
		||||
GB_SSD_PER_DAY=0.012
 | 
			
		||||
GB_HDD_PER_DAY=0.0006
 | 
			
		||||
| 
						 | 
				
			
			@ -16,14 +18,7 @@ class VMHostSerializer(serializers.ModelSerializer):
 | 
			
		|||
        fields = '__all__'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VMProductSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = VMProduct
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
 | 
			
		||||
class VMDiskProductSerializer(serializers.ModelSerializer):
 | 
			
		||||
#    vm = VMProductSerializer()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = VMDiskProduct
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
| 
						 | 
				
			
			@ -33,6 +28,25 @@ class VMDiskImageProductSerializer(serializers.ModelSerializer):
 | 
			
		|||
        model = VMDiskImageProduct
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
 | 
			
		||||
class VMProductSerializer(serializers.HyperlinkedModelSerializer):
 | 
			
		||||
    # Custom field used at creation (= ordering) only.
 | 
			
		||||
    recurring_period = serializers.ChoiceField(
 | 
			
		||||
            choices=VMProduct.allowed_recurring_periods())
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = VMProduct
 | 
			
		||||
        fields = ['uuid', 'order', 'owner', 'status', 'name', \
 | 
			
		||||
                'cores', 'ram_in_gb', 'recurring_period']
 | 
			
		||||
        read_only_fields = ['uuid', 'order', 'owner', 'status']
 | 
			
		||||
 | 
			
		||||
class ManagedVMProductSerializer(serializers.ModelSerializer):
 | 
			
		||||
    """
 | 
			
		||||
    Managed VM serializer used in ungleich_service app.
 | 
			
		||||
    """
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = VMProduct
 | 
			
		||||
        fields = [ 'cores', 'ram_in_gb']
 | 
			
		||||
 | 
			
		||||
class VMSnapshotProductSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = VMSnapshotProduct
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
from django.db import transaction
 | 
			
		||||
from django.shortcuts import render
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
| 
						 | 
				
			
			@ -7,11 +8,11 @@ from rest_framework import viewsets, permissions
 | 
			
		|||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.exceptions import ValidationError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from .models import VMHost, VMProduct, VMSnapshotProduct, VMDiskProduct, VMDiskImageProduct
 | 
			
		||||
from uncloud_pay.models import Order
 | 
			
		||||
 | 
			
		||||
from .serializers import VMHostSerializer, VMProductSerializer, VMSnapshotProductSerializer, VMDiskImageProductSerializer, VMDiskProductSerializer
 | 
			
		||||
from uncloud_pay.helpers import ProductViewSet
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import datetime
 | 
			
		||||
| 
						 | 
				
			
			@ -77,27 +78,37 @@ class VMDiskProductViewSet(viewsets.ModelViewSet):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VMProductViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class VMProductViewSet(ProductViewSet):
 | 
			
		||||
    permission_classes = [permissions.IsAuthenticated]
 | 
			
		||||
    serializer_class = VMProductSerializer
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return VMProduct.objects.filter(owner=self.request.user)
 | 
			
		||||
 | 
			
		||||
    # Use a database transaction so that we do not get half-created structure
 | 
			
		||||
    # if something goes wrong.
 | 
			
		||||
    @transaction.atomic
 | 
			
		||||
    def create(self, request):
 | 
			
		||||
        # Extract serializer data.
 | 
			
		||||
        serializer = VMProductSerializer(data=request.data, context={'request': request})
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        # Create order
 | 
			
		||||
        now = datetime.datetime.now()
 | 
			
		||||
        order = Order(owner=request.user,
 | 
			
		||||
                      creation_date=now,
 | 
			
		||||
                      starting_date=now,
 | 
			
		||||
                      recurring_price=20,
 | 
			
		||||
                      one_time_price=0,
 | 
			
		||||
                      recurring_period="per_month")
 | 
			
		||||
        order_recurring_period = serializer.validated_data.pop("recurring_period")
 | 
			
		||||
 | 
			
		||||
        # Create base order.
 | 
			
		||||
        order = Order.objects.create(
 | 
			
		||||
                recurring_period=order_recurring_period,
 | 
			
		||||
                owner=request.user
 | 
			
		||||
                )
 | 
			
		||||
        order.save()
 | 
			
		||||
 | 
			
		||||
        serializer.save(owner=request.user, order=order)
 | 
			
		||||
        # Create VM.
 | 
			
		||||
        vm = serializer.save(owner=request.user, order=order)
 | 
			
		||||
 | 
			
		||||
        # Add Product record to order (VM is mutable, allows to keep history in order).
 | 
			
		||||
        # XXX: Move this to some kind of on_create hook in parent Product class?
 | 
			
		||||
        order.add_record(vm.setup_fee,
 | 
			
		||||
                vm.recurring_price(order.recurring_period), vm.description)
 | 
			
		||||
 | 
			
		||||
        return Response(serializer.data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										0
									
								
								uncloud/ungleich_service/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								uncloud/ungleich_service/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										3
									
								
								uncloud/ungleich_service/admin.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								uncloud/ungleich_service/admin.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
# Register your models here.
 | 
			
		||||
							
								
								
									
										5
									
								
								uncloud/ungleich_service/apps.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								uncloud/ungleich_service/apps.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UngleichServiceConfig(AppConfig):
 | 
			
		||||
    name = 'ungleich_service'
 | 
			
		||||
							
								
								
									
										33
									
								
								uncloud/ungleich_service/migrations/0001_initial.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								uncloud/ungleich_service/migrations/0001_initial.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-28 13:44
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    initial = True
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('uncloud_pay', '0010_merge_20200228_1303'),
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
        ('uncloud_vm', '0007_auto_20200228_1344'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='MatrixServiceProduct',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
 | 
			
		||||
                ('status', models.CharField(choices=[('pending', 'Pending'), ('being_created', 'Being created'), ('active', 'Active'), ('deleted', 'Deleted')], default='pending', max_length=256)),
 | 
			
		||||
                ('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)),
 | 
			
		||||
                ('vm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMProduct')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'abstract': False,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
# Generated by Django 3.0.3 on 2020-02-28 14:16
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('ungleich_service', '0001_initial'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='matrixserviceproduct',
 | 
			
		||||
            name='domain',
 | 
			
		||||
            field=models.CharField(default='domain.tld', max_length=255),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										0
									
								
								uncloud/ungleich_service/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								uncloud/ungleich_service/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										32
									
								
								uncloud/ungleich_service/models.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								uncloud/ungleich_service/models.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
import uuid
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from uncloud_pay.models import Product, RecurringPeriod
 | 
			
		||||
from uncloud_vm.models import VMProduct
 | 
			
		||||
 | 
			
		||||
class MatrixServiceProduct(Product):
 | 
			
		||||
    monthly_managment_fee = 20
 | 
			
		||||
 | 
			
		||||
    description = "Managed Matrix HomeServer"
 | 
			
		||||
 | 
			
		||||
    # Specific to Matrix-as-a-Service
 | 
			
		||||
    vm = models.ForeignKey(
 | 
			
		||||
            VMProduct, on_delete=models.CASCADE
 | 
			
		||||
            )
 | 
			
		||||
    domain = models.CharField(max_length=255, default='domain.tld')
 | 
			
		||||
 | 
			
		||||
    def recurring_price(self, recurring_period=RecurringPeriod.PER_MONTH):
 | 
			
		||||
        if recurring_period == RecurringPeriod.PER_MONTH:
 | 
			
		||||
            return self.monthly_managment_fee
 | 
			
		||||
        else:
 | 
			
		||||
            raise Exception('Invalid recurring period for VM Product pricing.')
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def allowed_recurring_periods():
 | 
			
		||||
        return list(filter(
 | 
			
		||||
            lambda pair: pair[0] in [RecurringPeriod.PER_MONTH],
 | 
			
		||||
            RecurringPeriod.choices))
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def setup_fee(self):
 | 
			
		||||
        return 30
 | 
			
		||||
							
								
								
									
										17
									
								
								uncloud/ungleich_service/serializers.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								uncloud/ungleich_service/serializers.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
from rest_framework import serializers
 | 
			
		||||
from .models import MatrixServiceProduct
 | 
			
		||||
from uncloud_vm.serializers import ManagedVMProductSerializer
 | 
			
		||||
from uncloud_vm.models import VMProduct
 | 
			
		||||
from uncloud_pay.models import RecurringPeriod
 | 
			
		||||
 | 
			
		||||
class MatrixServiceProductSerializer(serializers.ModelSerializer):
 | 
			
		||||
    vm = ManagedVMProductSerializer()
 | 
			
		||||
 | 
			
		||||
    # Custom field used at creation (= ordering) only.
 | 
			
		||||
    recurring_period = serializers.ChoiceField(
 | 
			
		||||
            choices=MatrixServiceProduct.allowed_recurring_periods())
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = MatrixServiceProduct
 | 
			
		||||
        fields = ['uuid', 'order', 'owner', 'status', 'vm', 'domain', 'recurring_period']
 | 
			
		||||
        read_only_fields = ['uuid', 'order', 'owner', 'status']
 | 
			
		||||
							
								
								
									
										3
									
								
								uncloud/ungleich_service/tests.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								uncloud/ungleich_service/tests.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
# Create your tests here.
 | 
			
		||||
							
								
								
									
										61
									
								
								uncloud/ungleich_service/views.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								uncloud/ungleich_service/views.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
from rest_framework import viewsets, permissions
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from django.db import transaction
 | 
			
		||||
 | 
			
		||||
from .models import MatrixServiceProduct
 | 
			
		||||
from .serializers import MatrixServiceProductSerializer
 | 
			
		||||
 | 
			
		||||
from uncloud_pay.helpers import ProductViewSet
 | 
			
		||||
from uncloud_pay.models import Order
 | 
			
		||||
from uncloud_vm.models import VMProduct
 | 
			
		||||
 | 
			
		||||
class MatrixServiceProductViewSet(ProductViewSet):
 | 
			
		||||
    permission_classes = [permissions.IsAuthenticated]
 | 
			
		||||
    serializer_class = MatrixServiceProductSerializer
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return MatrixServiceProduct.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
 | 
			
		||||
                )
 | 
			
		||||
        order.save()
 | 
			
		||||
 | 
			
		||||
        # Create unerderlying VM.
 | 
			
		||||
        # TODO: move this logic to a method for use with other
 | 
			
		||||
        # products.
 | 
			
		||||
        vm_data = serializer.validated_data.pop('vm')
 | 
			
		||||
        vm_data['owner'] = request.user
 | 
			
		||||
        vm_data['order'] = order
 | 
			
		||||
        vm = VMProduct.objects.create(**vm_data)
 | 
			
		||||
 | 
			
		||||
        # XXX: Move this to some kind of on_create hook in parent
 | 
			
		||||
        # Product class?
 | 
			
		||||
        order.add_record(
 | 
			
		||||
                vm.setup_fee,
 | 
			
		||||
                vm.recurring_price(order.recurring_period),
 | 
			
		||||
                vm.description)
 | 
			
		||||
 | 
			
		||||
        # Create service.
 | 
			
		||||
        service = serializer.save(
 | 
			
		||||
                order=order,
 | 
			
		||||
                owner=self.request.user,
 | 
			
		||||
                vm=vm)
 | 
			
		||||
 | 
			
		||||
        # XXX: Move this to some kind of on_create hook in parent
 | 
			
		||||
        # Product class?
 | 
			
		||||
        order.add_record(
 | 
			
		||||
                service.setup_fee,
 | 
			
		||||
                service.recurring_price(order.recurring_period),
 | 
			
		||||
                service.description)
 | 
			
		||||
 | 
			
		||||
        return Response(serializer.data)
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue