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_ADMIN_PASSWORD=""
 | 
				
			||||||
LDAP_SERVER_URI = ""
 | 
					LDAP_SERVER_URI = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Stripe (Credit Card payments)
 | 
				
			||||||
 | 
					STRIPE_API_key=""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SECRET_KEY="dx$iqt=lc&yrp^!z5$ay^%g5lhx1y3bcu=jg(jx0yj0ogkfqvf"
 | 
					SECRET_KEY="dx$iqt=lc&yrp^!z5$ay^%g5lhx1y3bcu=jg(jx0yj0ogkfqvf"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -62,7 +62,9 @@ INSTALLED_APPS = [
 | 
				
			||||||
    'rest_framework',
 | 
					    'rest_framework',
 | 
				
			||||||
    'uncloud_pay',
 | 
					    'uncloud_pay',
 | 
				
			||||||
    'uncloud_auth',
 | 
					    'uncloud_auth',
 | 
				
			||||||
 | 
					    'uncloud_storage',
 | 
				
			||||||
    'uncloud_vm',
 | 
					    'uncloud_vm',
 | 
				
			||||||
 | 
					    'ungleich_service',
 | 
				
			||||||
    'opennebula'
 | 
					    'opennebula'
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -174,3 +176,8 @@ USE_TZ = True
 | 
				
			||||||
STATIC_URL = '/static/'
 | 
					STATIC_URL = '/static/'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stripe.api_key = uncloud.secrets.STRIPE_KEY
 | 
					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_vm import views as vmviews
 | 
				
			||||||
from uncloud_pay import views as payviews
 | 
					from uncloud_pay import views as payviews
 | 
				
			||||||
 | 
					from ungleich_service import views as serviceviews
 | 
				
			||||||
from opennebula import views as oneviews
 | 
					from opennebula import views as oneviews
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router = routers.DefaultRouter()
 | 
					router = routers.DefaultRouter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# user / regular urls
 | 
					# VM
 | 
				
			||||||
router.register(r'vm/snapshot', vmviews.VMSnapshotProductViewSet, basename='vmsnapshotproduct')
 | 
					router.register(r'vm/snapshot', vmviews.VMSnapshotProductViewSet, basename='vmsnapshotproduct')
 | 
				
			||||||
router.register(r'vm/disk', vmviews.VMDiskProductViewSet, basename='vmdiskproduct')
 | 
					router.register(r'vm/disk', vmviews.VMDiskProductViewSet, basename='vmdiskproduct')
 | 
				
			||||||
router.register(r'vm/image/mine', vmviews.VMDiskImageProductMineViewSet, basename='vmdiskimagemineproduct')
 | 
					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')
 | 
					router.register(r'vm/vm', vmviews.VMProductViewSet, basename='vmproduct')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# TBD
 | 
					# TBD
 | 
				
			||||||
#router.register(r'vm/disk', vmviews.VMDiskProductViewSet, basename='vmdiskproduct')
 | 
					#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
 | 
					# allow vm creation from own images
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Services
 | 
				
			||||||
 | 
					router.register(r'service/matrix', serviceviews.MatrixServiceProductViewSet, basename='matrixserviceproduct')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Pay
 | 
					# Pay
 | 
				
			||||||
router.register(r'user', payviews.UserViewSet, basename='user')
 | 
					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'bill', payviews.BillViewSet, basename='bill')
 | 
				
			||||||
router.register(r'order', payviews.OrderViewSet, basename='order')
 | 
					router.register(r'order', payviews.OrderViewSet, basename='order')
 | 
				
			||||||
router.register(r'payment', payviews.PaymentViewSet, basename='payment')
 | 
					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
 | 
					# admin/staff urls
 | 
				
			||||||
router.register(r'admin/bill', payviews.AdminBillViewSet, basename='admin/bill')
 | 
					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/order', payviews.AdminOrderViewSet, basename='admin/order')
 | 
				
			||||||
router.register(r'admin/vmhost', vmviews.VMHostViewSet)
 | 
					router.register(r'admin/vmhost', vmviews.VMHostViewSet)
 | 
				
			||||||
router.register(r'admin/opennebula', oneviews.VMViewSet, basename='opennebula')
 | 
					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 = [
 | 
					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 django.db import models
 | 
				
			||||||
 | 
					from functools import reduce
 | 
				
			||||||
from django.contrib.auth import get_user_model
 | 
					from django.contrib.auth import get_user_model
 | 
				
			||||||
from django.core.validators import MinValueValidator
 | 
					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
 | 
					import uuid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Define DecimalField properties, used to represent amounts of money.
 | 
				
			||||||
AMOUNT_MAX_DIGITS=10
 | 
					AMOUNT_MAX_DIGITS=10
 | 
				
			||||||
AMOUNT_DECIMALS=2
 | 
					AMOUNT_DECIMALS=2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Bill(models.Model):
 | 
					# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
 | 
				
			||||||
    owner = models.ForeignKey(get_user_model(),
 | 
					class RecurringPeriod(models.TextChoices):
 | 
				
			||||||
            on_delete=models.CASCADE,
 | 
					    ONE_TIME   = 'ONCE', _('Onetime')
 | 
				
			||||||
            editable=False)
 | 
					    PER_YEAR   = 'YEAR', _('Per Year')
 | 
				
			||||||
 | 
					    PER_MONTH  = 'MONTH', _('Per Month')
 | 
				
			||||||
    creation_date = models.DateTimeField()
 | 
					    PER_MINUTE = 'MINUTE', _('Per Minute')
 | 
				
			||||||
    starting_date = models.DateTimeField()
 | 
					    PER_DAY    = 'DAY', _('Per Day')
 | 
				
			||||||
    ending_date = models.DateTimeField()
 | 
					    PER_HOUR   = 'HOUR', _('Per Hour')
 | 
				
			||||||
    due_date = models.DateField()
 | 
					    PER_SECOND = 'SECOND', _('Per Second')
 | 
				
			||||||
 | 
					 | 
				
			||||||
    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
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					###
 | 
				
			||||||
 | 
					# Payments and Payment Methods.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Payment(models.Model):
 | 
					class Payment(models.Model):
 | 
				
			||||||
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
 | 
					    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    owner = models.ForeignKey(get_user_model(),
 | 
					    owner = models.ForeignKey(get_user_model(),
 | 
				
			||||||
            on_delete=models.CASCADE,
 | 
					            on_delete=models.CASCADE)
 | 
				
			||||||
            editable=False)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    amount = models.DecimalField(
 | 
					    amount = models.DecimalField(
 | 
				
			||||||
            default=0.0,
 | 
					            default=0.0,
 | 
				
			||||||
| 
						 | 
					@ -92,11 +48,226 @@ class Payment(models.Model):
 | 
				
			||||||
                                  ('unknown', 'Unknown')
 | 
					                                  ('unknown', 'Unknown')
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
                              default='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):
 | 
					class Product(models.Model):
 | 
				
			||||||
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
 | 
					    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
 | 
				
			||||||
    owner = models.ForeignKey(get_user_model(),
 | 
					    owner = models.ForeignKey(get_user_model(),
 | 
				
			||||||
| 
						 | 
					@ -117,7 +288,24 @@ class Product(models.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    order = models.ForeignKey(Order,
 | 
					    order = models.ForeignKey(Order,
 | 
				
			||||||
                              on_delete=models.CASCADE,
 | 
					                              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:
 | 
					    class Meta:
 | 
				
			||||||
        abstract = True
 | 
					        abstract = True
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,27 +1,125 @@
 | 
				
			||||||
from django.contrib.auth import get_user_model
 | 
					from django.contrib.auth import get_user_model
 | 
				
			||||||
from rest_framework import serializers
 | 
					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:
 | 
					    class Meta:
 | 
				
			||||||
        model = Bill
 | 
					        model = get_user_model()
 | 
				
			||||||
        fields = ['owner', 'amount', 'due_date', 'creation_date',
 | 
					        fields = ['username', 'email', 'balance']
 | 
				
			||||||
                'starting_date', 'ending_date', 'paid']
 | 
					
 | 
				
			||||||
 | 
					    # 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 PaymentSerializer(serializers.ModelSerializer):
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Payment
 | 
					        model = Payment
 | 
				
			||||||
        fields = ['owner', 'amount', 'source', 'timestamp']
 | 
					        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):
 | 
					class OrderSerializer(serializers.ModelSerializer):
 | 
				
			||||||
 | 
					    records = OrderRecordSerializer(many=True, read_only=True)
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Order
 | 
					        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 ProductSerializer(serializers.Serializer):
 | 
				
			||||||
    class Meta:
 | 
					    vms = VMProductSerializer(many=True, read_only=True)
 | 
				
			||||||
        model = get_user_model()
 | 
					 | 
				
			||||||
        fields = ['username', 'email']
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_balance(self, obj):
 | 
					 | 
				
			||||||
        return 666
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,16 @@
 | 
				
			||||||
import stripe
 | 
					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_stripe_error(f):
 | 
				
			||||||
    def handle_problems(*args, **kwargs):
 | 
					    def handle_problems(*args, **kwargs):
 | 
				
			||||||
        response = {
 | 
					        response = {
 | 
				
			||||||
| 
						 | 
					@ -53,3 +64,51 @@ def handle_stripe_error(f):
 | 
				
			||||||
            return response
 | 
					            return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return handle_problems
 | 
					    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.response import Response
 | 
				
			||||||
from rest_framework.decorators import action
 | 
					from rest_framework.decorators import action
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Bill, Payment, Order
 | 
					import json
 | 
				
			||||||
from .serializers import BillSerializer, PaymentSerializer, UserSerializer, OrderSerializer
 | 
					
 | 
				
			||||||
 | 
					from .models import *
 | 
				
			||||||
 | 
					from .serializers import *
 | 
				
			||||||
from datetime import datetime
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
###
 | 
					###
 | 
				
			||||||
| 
						 | 
					@ -54,9 +56,39 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
 | 
				
			||||||
    def get_queryset(self):
 | 
					    def get_queryset(self):
 | 
				
			||||||
        return get_user_model().objects.all()
 | 
					        return get_user_model().objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @action(detail=True)
 | 
					class PaymentMethodViewSet(viewsets.ModelViewSet):
 | 
				
			||||||
    def balance(self, request):
 | 
					    permission_classes = [permissions.IsAuthenticated]
 | 
				
			||||||
        return Response(status=status.HTTP_204_NO_CONTENT)
 | 
					
 | 
				
			||||||
 | 
					    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.
 | 
					# Admin views.
 | 
				
			||||||
| 
						 | 
					@ -89,7 +121,7 @@ class AdminBillViewSet(viewsets.ModelViewSet):
 | 
				
			||||||
    def create(self, request):
 | 
					    def create(self, request):
 | 
				
			||||||
        serializer = self.get_serializer(data=request.data)
 | 
					        serializer = self.get_serializer(data=request.data)
 | 
				
			||||||
        serializer.is_valid(raise_exception=True)
 | 
					        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)
 | 
					        headers = self.get_success_headers(serializer.data)
 | 
				
			||||||
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
 | 
					        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',
 | 
					            model_name='vmsnapshotproduct',
 | 
				
			||||||
            name='vm_uuid',
 | 
					            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(
 | 
					        migrations.AddField(
 | 
				
			||||||
            model_name='vmsnapshotproduct',
 | 
					            model_name='vmsnapshotproduct',
 | 
				
			||||||
            name='vm',
 | 
					            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
 | 
					# Uncomment if you override model's clean method
 | 
				
			||||||
# from django.core.exceptions import ValidationError
 | 
					# 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 = (
 | 
					STATUS_CHOICES = (
 | 
				
			||||||
    ('pending', 'Pending'),   # Initial state
 | 
					    ('pending', 'Pending'),   # Initial state
 | 
				
			||||||
| 
						 | 
					@ -45,9 +46,31 @@ class VMProduct(Product):
 | 
				
			||||||
        VMHost, on_delete=models.CASCADE, editable=False, blank=True, null=True
 | 
					        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()
 | 
					    cores = models.IntegerField()
 | 
				
			||||||
    ram_in_gb = models.FloatField()
 | 
					    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):
 | 
					class VMWithOSProduct(VMProduct):
 | 
				
			||||||
    pass
 | 
					    pass
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,9 @@
 | 
				
			||||||
from django.contrib.auth import get_user_model
 | 
					from django.contrib.auth import get_user_model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from rest_framework import serializers
 | 
					from rest_framework import serializers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import VMHost, VMProduct, VMSnapshotProduct, VMDiskProduct, VMDiskImageProduct
 | 
					from .models import VMHost, VMProduct, VMSnapshotProduct, VMDiskProduct, VMDiskImageProduct
 | 
				
			||||||
 | 
					from uncloud_pay.models import RecurringPeriod
 | 
				
			||||||
 | 
					
 | 
				
			||||||
GB_SSD_PER_DAY=0.012
 | 
					GB_SSD_PER_DAY=0.012
 | 
				
			||||||
GB_HDD_PER_DAY=0.0006
 | 
					GB_HDD_PER_DAY=0.0006
 | 
				
			||||||
| 
						 | 
					@ -16,14 +18,7 @@ class VMHostSerializer(serializers.ModelSerializer):
 | 
				
			||||||
        fields = '__all__'
 | 
					        fields = '__all__'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VMProductSerializer(serializers.ModelSerializer):
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = VMProduct
 | 
					 | 
				
			||||||
        fields = '__all__'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class VMDiskProductSerializer(serializers.ModelSerializer):
 | 
					class VMDiskProductSerializer(serializers.ModelSerializer):
 | 
				
			||||||
#    vm = VMProductSerializer()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = VMDiskProduct
 | 
					        model = VMDiskProduct
 | 
				
			||||||
        fields = '__all__'
 | 
					        fields = '__all__'
 | 
				
			||||||
| 
						 | 
					@ -33,6 +28,25 @@ class VMDiskImageProductSerializer(serializers.ModelSerializer):
 | 
				
			||||||
        model = VMDiskImageProduct
 | 
					        model = VMDiskImageProduct
 | 
				
			||||||
        fields = '__all__'
 | 
					        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 VMSnapshotProductSerializer(serializers.ModelSerializer):
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = VMSnapshotProduct
 | 
					        model = VMSnapshotProduct
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					from django.db import transaction
 | 
				
			||||||
from django.shortcuts import render
 | 
					from django.shortcuts import render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib.auth.models import User
 | 
					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.response import Response
 | 
				
			||||||
from rest_framework.exceptions import ValidationError
 | 
					from rest_framework.exceptions import ValidationError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
from .models import VMHost, VMProduct, VMSnapshotProduct, VMDiskProduct, VMDiskImageProduct
 | 
					from .models import VMHost, VMProduct, VMSnapshotProduct, VMDiskProduct, VMDiskImageProduct
 | 
				
			||||||
from uncloud_pay.models import Order
 | 
					from uncloud_pay.models import Order
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .serializers import VMHostSerializer, VMProductSerializer, VMSnapshotProductSerializer, VMDiskImageProductSerializer, VMDiskProductSerializer
 | 
					from .serializers import VMHostSerializer, VMProductSerializer, VMSnapshotProductSerializer, VMDiskImageProductSerializer, VMDiskProductSerializer
 | 
				
			||||||
 | 
					from uncloud_pay.helpers import ProductViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
| 
						 | 
					@ -77,27 +78,37 @@ class VMDiskProductViewSet(viewsets.ModelViewSet):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VMProductViewSet(viewsets.ModelViewSet):
 | 
					class VMProductViewSet(ProductViewSet):
 | 
				
			||||||
    permission_classes = [permissions.IsAuthenticated]
 | 
					    permission_classes = [permissions.IsAuthenticated]
 | 
				
			||||||
    serializer_class = VMProductSerializer
 | 
					    serializer_class = VMProductSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self):
 | 
					    def get_queryset(self):
 | 
				
			||||||
        return VMProduct.objects.filter(owner=self.request.user)
 | 
					        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):
 | 
					    def create(self, request):
 | 
				
			||||||
 | 
					        # Extract serializer data.
 | 
				
			||||||
        serializer = VMProductSerializer(data=request.data, context={'request': request})
 | 
					        serializer = VMProductSerializer(data=request.data, context={'request': request})
 | 
				
			||||||
        serializer.is_valid(raise_exception=True)
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
        # Create order
 | 
					        order_recurring_period = serializer.validated_data.pop("recurring_period")
 | 
				
			||||||
        now = datetime.datetime.now()
 | 
					
 | 
				
			||||||
        order = Order(owner=request.user,
 | 
					        # Create base order.
 | 
				
			||||||
                      creation_date=now,
 | 
					        order = Order.objects.create(
 | 
				
			||||||
                      starting_date=now,
 | 
					                recurring_period=order_recurring_period,
 | 
				
			||||||
                      recurring_price=20,
 | 
					                owner=request.user
 | 
				
			||||||
                      one_time_price=0,
 | 
					                )
 | 
				
			||||||
                      recurring_period="per_month")
 | 
					 | 
				
			||||||
        order.save()
 | 
					        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)
 | 
					        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