forked from uncloud/uncloud
- Added PricingPlan Model
- Implement a complete cycle for buying a Matrix Chat Host - Refactor the Payement cycle and stripe related methods
This commit is contained in:
parent
e205d8d07c
commit
b7aa1c6971
81 changed files with 5079 additions and 810 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from math import ceil
|
||||
from calendar import monthrange
|
||||
|
|
@ -9,18 +10,22 @@ from django.conf import settings
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
|
||||
from django_q.tasks import schedule
|
||||
from django_q.models import Schedule
|
||||
# Verify whether or not to use them here
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
|
||||
import uncloud_pay
|
||||
from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS
|
||||
from uncloud.models import UncloudAddress
|
||||
from uncloud.models import UncloudAddress, UncloudProvider
|
||||
from uncloud.selectors import filter_for_when
|
||||
from .services import *
|
||||
|
||||
# Used to generate bill due dates.
|
||||
BILL_PAYMENT_DELAY=datetime.timedelta(days=10)
|
||||
BILL_PAYMENT_DELAY=datetime.timedelta(days=settings.BILL_PAYMENT_DELAY)
|
||||
|
||||
# Initialize logger.
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -96,84 +101,18 @@ class Payment(models.Model):
|
|||
def __str__(self):
|
||||
return f"{self.amount}{self.currency} from {self.owner} via {self.source} on {self.timestamp}"
|
||||
|
||||
###
|
||||
# Payments and Payment Methods.
|
||||
|
||||
|
||||
class PaymentMethod(models.Model):
|
||||
"""
|
||||
Not sure if this is still in use
|
||||
|
||||
"""
|
||||
|
||||
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=False, editable=False)
|
||||
|
||||
# Only used for "Stripe" source
|
||||
stripe_payment_method_id = models.CharField(max_length=32, blank=True, null=True)
|
||||
stripe_setup_intent_id = models.CharField(max_length=32, blank=True, null=True)
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
if self.source == 'stripe' and self.stripe_payment_method_id != None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def charge(self, amount):
|
||||
if not self.active:
|
||||
raise Exception('This payment method is inactive.')
|
||||
|
||||
if amount < 0: # Make sure we don't charge negative amount by errors...
|
||||
raise Exception('Cannot charge negative amount.')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Try to charge the user via the active card before saving otherwise throw payment Error
|
||||
if self.source == 'stripe':
|
||||
stripe_customer = StripeCustomer.objects.get(owner=self.owner).stripe_id
|
||||
stripe_payment = uncloud_pay.stripe.charge_customer(
|
||||
amount, stripe_customer, self.stripe_payment_method_id)
|
||||
if 'paid' in stripe_payment and stripe_payment['paid'] == False:
|
||||
raise Exception(stripe_payment['error'])
|
||||
else:
|
||||
payment = Payment.objects.create(
|
||||
owner=self.owner, source=self.source, amount=amount)
|
||||
|
||||
return payment
|
||||
else:
|
||||
raise Exception('This payment method is unsupported/cannot be charged.')
|
||||
|
||||
def set_as_primary_for(self, user):
|
||||
methods = PaymentMethod.objects.filter(owner=user, primary=True)
|
||||
for method in methods:
|
||||
print(method)
|
||||
method.primary = False
|
||||
method.save()
|
||||
|
||||
self.primary = True
|
||||
self.save()
|
||||
|
||||
def get_primary_for(user):
|
||||
methods = PaymentMethod.objects.filter(owner=user)
|
||||
for method in methods:
|
||||
# Do we want to do something with non-primary method?
|
||||
if method.active and method.primary:
|
||||
return method
|
||||
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
# TODO: limit to one primary method per user.
|
||||
# unique_together is no good since it won't allow more than one
|
||||
# non-primary method.
|
||||
pass
|
||||
try:
|
||||
result = uncloud_pay.stripe.charge_customer(self.owner, self.amount, self.currency,)
|
||||
if not result.status or result.status != 'succeeded':
|
||||
raise Exception("The payment has been failed, please try to activate another card")
|
||||
super().save(*args, **kwargs)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
|
||||
|
||||
# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
|
||||
class RecurringPeriodDefaultChoices(models.IntegerChoices):
|
||||
|
|
@ -231,9 +170,11 @@ class RecurringPeriod(models.Model):
|
|||
# Bills.
|
||||
|
||||
class BillingAddress(UncloudAddress):
|
||||
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
||||
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='billing_addresses')
|
||||
vat_number = models.CharField(max_length=100, default="", blank=True)
|
||||
vat_number_verified = models.BooleanField(default=False)
|
||||
vat_number_validated_on = models.DateTimeField(blank=True, null=True)
|
||||
stripe_tax_id = models.CharField(max_length=100, default="", blank=True)
|
||||
active = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
|
|
@ -272,6 +213,10 @@ class BillingAddress(UncloudAddress):
|
|||
self.owner,
|
||||
self.full_name, self.street, self.postal_code, self.city,
|
||||
self.country)
|
||||
|
||||
@staticmethod
|
||||
def get_address_for(user):
|
||||
return BillingAddress.objects.get(owner=user)
|
||||
|
||||
###
|
||||
# VAT
|
||||
|
|
@ -297,10 +242,44 @@ class VATRate(models.Model):
|
|||
logger.debug(str(dne))
|
||||
logger.debug("Did not find VAT rate for %s, returning 0" % country_code)
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def get_vat_rate(billing_address, when=None):
|
||||
"""
|
||||
Returns the VAT rate for business to customer.
|
||||
|
||||
B2B is always 0% with the exception of trading within the own country
|
||||
"""
|
||||
|
||||
country = billing_address.country
|
||||
|
||||
# Need to have a provider country
|
||||
providers = UncloudProvider.objects.all()
|
||||
vatrate = filter_for_when(VATRate.objects.filter(territory_codes=country), when).first()
|
||||
|
||||
if not providers and not vatrate:
|
||||
return 0
|
||||
|
||||
uncloud_provider = filter_for_when(providers).get()
|
||||
|
||||
# By default we charge VAT. This affects:
|
||||
# - Same country sales (VAT applied)
|
||||
# - B2C to EU (VAT applied)
|
||||
rate = vatrate.rate if vatrate else 0
|
||||
|
||||
# Exception: if...
|
||||
# - the billing_address is in EU,
|
||||
# - the vat_number has been set
|
||||
# - the vat_number has been verified
|
||||
# Then we do not charge VAT
|
||||
|
||||
if uncloud_provider.country != country and billing_address.vat_number and billing_address.vat_number_verified:
|
||||
rate = 0
|
||||
return rate
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.territory_codes}: {self.starting_date} - {self.ending_date}: {self.rate_type}"
|
||||
return f"{self.territory_codes}: {self.starting_date} - {self.ending_date or ''}: {self.rate_type}"
|
||||
|
||||
###
|
||||
# Products
|
||||
|
|
@ -342,30 +321,20 @@ class Product(models.Model):
|
|||
'features': {
|
||||
'cores':
|
||||
{ 'min': 1,
|
||||
'max': 48,
|
||||
'one_time_price_per_unit': 0,
|
||||
'recurring_price_per_unit': 3
|
||||
'max': 48
|
||||
},
|
||||
'ram_gb':
|
||||
{ 'min': 1,
|
||||
'max': 256,
|
||||
'one_time_price_per_unit': 0,
|
||||
'recurring_price_per_unit': 4
|
||||
'max': 256
|
||||
},
|
||||
'ssd_gb':
|
||||
{ 'min': 10,
|
||||
'one_time_price_per_unit': 0,
|
||||
'recurring_price_per_unit': 0.35
|
||||
{ 'min': 10
|
||||
},
|
||||
'hdd_gb':
|
||||
{ 'min': 0,
|
||||
'one_time_price_per_unit': 0,
|
||||
'recurring_price_per_unit': 15/1000
|
||||
},
|
||||
'additional_ipv4_address':
|
||||
{ 'min': 0,
|
||||
'one_time_price_per_unit': 0,
|
||||
'recurring_price_per_unit': 8
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -381,36 +350,23 @@ class Product(models.Model):
|
|||
'base':
|
||||
{ 'min': 1,
|
||||
'max': 1,
|
||||
'one_time_price_per_unit': 0,
|
||||
'recurring_price_per_unit': 1
|
||||
},
|
||||
'cores':
|
||||
{ 'min': 1,
|
||||
'max': 48,
|
||||
'one_time_price_per_unit': 0,
|
||||
'recurring_price_per_unit': 3
|
||||
},
|
||||
'ram_gb':
|
||||
{ 'min': 1,
|
||||
'max': 256,
|
||||
'one_time_price_per_unit': 0,
|
||||
'recurring_price_per_unit': 4
|
||||
},
|
||||
'ssd_gb':
|
||||
{ 'min': 10,
|
||||
'one_time_price_per_unit': 0,
|
||||
'recurring_price_per_unit': 0.35
|
||||
{ 'min': 10
|
||||
},
|
||||
'hdd_gb':
|
||||
{ 'min': 0,
|
||||
'one_time_price_per_unit': 0,
|
||||
'recurring_price_per_unit': 15/1000
|
||||
{ 'min': 0
|
||||
},
|
||||
'additional_ipv4_address':
|
||||
{ 'min': 0,
|
||||
'one_time_price_per_unit': 0,
|
||||
'recurring_price_per_unit': 9
|
||||
},
|
||||
{ 'min': 0,},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -433,7 +389,7 @@ class Product(models.Model):
|
|||
|
||||
@property
|
||||
def recurring_orders(self):
|
||||
return self.orders.order_by('id').exclude(recurring_period=RecurringPeriod.objects.get(name="ONE_TIME"))
|
||||
return self.orders.order_by('id').exclude(recurring_price=0)
|
||||
|
||||
@property
|
||||
def last_recurring_order(self):
|
||||
|
|
@ -441,56 +397,12 @@ class Product(models.Model):
|
|||
|
||||
@property
|
||||
def one_time_orders(self):
|
||||
return self.orders.order_by('id').filter(recurring_period=RecurringPeriod.objects.get(name="ONE_TIME"))
|
||||
return self.orders.order_by('id').filter(recurring_price=0)
|
||||
|
||||
@property
|
||||
def last_one_time_order(self):
|
||||
return self.one_time_orders.last()
|
||||
|
||||
def create_order(self, when_to_start=None, recurring_period=None):
|
||||
billing_address = BillingAddress.get_address_for(self.owner)
|
||||
|
||||
if not billing_address:
|
||||
raise ValidationError("Cannot order without a billing address")
|
||||
|
||||
if not when_to_start:
|
||||
when_to_start = timezone.now()
|
||||
|
||||
if not recurring_period:
|
||||
recurring_period = self.default_recurring_period
|
||||
|
||||
|
||||
# Create one time order if we did not create one already
|
||||
if self.one_time_price > 0 and not self.last_one_time_order:
|
||||
one_time_order = Order.objects.create(owner=self.owner,
|
||||
billing_address=billing_address,
|
||||
starting_date=when_to_start,
|
||||
price=self.one_time_price,
|
||||
recurring_period=RecurringPeriod.objects.get(name="ONE_TIME"),
|
||||
description=str(self))
|
||||
self.orders.add(one_time_order)
|
||||
else:
|
||||
one_time_order = None
|
||||
|
||||
if recurring_period != RecurringPeriod.objects.get(name="ONE_TIME"):
|
||||
if one_time_order:
|
||||
recurring_order = Order.objects.create(owner=self.owner,
|
||||
billing_address=billing_address,
|
||||
starting_date=when_to_start,
|
||||
price=self.recurring_price,
|
||||
recurring_period=recurring_period,
|
||||
depends_on=one_time_order,
|
||||
description=str(self))
|
||||
else:
|
||||
recurring_order = Order.objects.create(owner=self.owner,
|
||||
billing_address=billing_address,
|
||||
starting_date=when_to_start,
|
||||
price=self.recurring_price,
|
||||
recurring_period=recurring_period,
|
||||
description=str(self))
|
||||
self.orders.add(recurring_order)
|
||||
|
||||
|
||||
# FIXME: this could/should be part of Order (?)
|
||||
def create_or_update_recurring_order(self, when_to_start=None, recurring_period=None):
|
||||
if not self.recurring_price:
|
||||
|
|
@ -618,10 +530,83 @@ class Product(models.Model):
|
|||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
###
|
||||
# Pricing
|
||||
######
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PricingPlan(models.Model):
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
vat_inclusive = models.BooleanField(default=True)
|
||||
vat_percentage = models.DecimalField(
|
||||
max_digits=7, decimal_places=5, blank=True, default=0
|
||||
)
|
||||
set_up_fees = models.DecimalField(
|
||||
max_digits=7, decimal_places=2, default=0
|
||||
)
|
||||
cores_unit_price = models.DecimalField(
|
||||
max_digits=7, decimal_places=2, default=0
|
||||
)
|
||||
ram_unit_price = models.DecimalField(
|
||||
max_digits=7, decimal_places=2, default=0
|
||||
)
|
||||
storage_unit_price = models.DecimalField(
|
||||
max_digits=7, decimal_places=2, default=0
|
||||
)
|
||||
discount_name = models.CharField(max_length=255, null=True, blank=True)
|
||||
discount_amount = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, default=0
|
||||
)
|
||||
stripe_coupon_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
display_str = self.name + ' => ' + ' - '.join([
|
||||
'{} Setup'.format(self.set_up_fees.normalize()),
|
||||
'{}/Core'.format(self.cores_unit_price.normalize()),
|
||||
'{}/GB RAM'.format(self.ram_unit_price.normalize()),
|
||||
'{}/GB SSD'.format(self.storage_unit_price.normalize()),
|
||||
'{}% VAT'.format(self.vat_percentage.normalize())
|
||||
if not self.vat_inclusive else 'VAT-Incl',
|
||||
])
|
||||
if self.discount_amount:
|
||||
display_str = ' - '.join([
|
||||
display_str,
|
||||
'{} {}'.format(
|
||||
self.discount_amount,
|
||||
self.discount_name if self.discount_name else 'Discount'
|
||||
)
|
||||
])
|
||||
return display_str
|
||||
|
||||
@classmethod
|
||||
def get_by_name(cls, name):
|
||||
try:
|
||||
pricing = PricingPlan.objects.get(name=name)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error getting VMPricing with name {name}. "
|
||||
"Details: {details}. Attempting to return default"
|
||||
"pricing.".format(name=name, details=str(e))
|
||||
)
|
||||
pricing = PricingPlan.get_default_pricing()
|
||||
return pricing
|
||||
|
||||
@classmethod
|
||||
def get_default_pricing(cls):
|
||||
""" Returns the default pricing or None """
|
||||
try:
|
||||
default_pricing = PricingPlan.objects.get(name='default')
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
default_pricing = None
|
||||
return default_pricing
|
||||
|
||||
###
|
||||
# Orders.
|
||||
|
||||
class Order(models.Model):
|
||||
"""
|
||||
Order are assumed IMMUTABLE and used as SOURCE OF TRUST for generating
|
||||
|
|
@ -650,6 +635,8 @@ class Order(models.Model):
|
|||
billing_address = models.ForeignKey(BillingAddress,
|
||||
on_delete=models.CASCADE)
|
||||
|
||||
customer = models.ForeignKey(StripeCustomer, on_delete=models.CASCADE, null=True)
|
||||
|
||||
description = models.TextField()
|
||||
|
||||
product = models.ForeignKey(Product, blank=False, null=False, on_delete=models.CASCADE)
|
||||
|
|
@ -686,6 +673,7 @@ class Order(models.Model):
|
|||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True)
|
||||
pricing_plan = models.ForeignKey(PricingPlan, blank=False, null=True, on_delete=models.CASCADE)
|
||||
|
||||
should_be_billed = models.BooleanField(default=True)
|
||||
|
||||
|
|
@ -750,6 +738,17 @@ class Order(models.Model):
|
|||
"""
|
||||
|
||||
return sum([ br.quantity for br in self.bill_records.all() ])
|
||||
|
||||
def cancel(self):
|
||||
self.ending_date = timezone.now()
|
||||
self.should_be_billed = False
|
||||
self.save()
|
||||
if self.instance_id:
|
||||
last_bill_record = BillRecord.objects.filter(order=self).order_by('id').last()
|
||||
schedule('matrixhosting.tasks.delete_instance',
|
||||
self.instance_id,
|
||||
schedule_type=Schedule.ONCE,
|
||||
next_run=last_bill_record.ending_date or (timezone.now() + datetime.timedelta(hours=1)))
|
||||
|
||||
def count_used(self, when=None):
|
||||
"""
|
||||
|
|
@ -790,7 +789,7 @@ class Order(models.Model):
|
|||
|
||||
@property
|
||||
def is_recurring(self):
|
||||
return not self.recurring_period == RecurringPeriod.objects.get(name="ONE_TIME")
|
||||
return self.recurring_price > 0
|
||||
|
||||
@property
|
||||
def is_one_time(self):
|
||||
|
|
@ -814,14 +813,12 @@ class Order(models.Model):
|
|||
description=self.description,
|
||||
product=self.product,
|
||||
config=config,
|
||||
pricing_plan=self.pricing_plan,
|
||||
starting_date=starting_date,
|
||||
currency=self.currency
|
||||
)
|
||||
|
||||
(new_order.one_time_price, new_order.recurring_price, new_order.config) = new_order.calculate_prices_and_config()
|
||||
|
||||
|
||||
|
||||
new_order.recurring_price = new_order.calculate_recurring_price()
|
||||
new_order.replaces = self
|
||||
new_order.save()
|
||||
|
||||
|
|
@ -830,26 +827,28 @@ class Order(models.Model):
|
|||
|
||||
return new_order
|
||||
|
||||
|
||||
|
||||
def create_bill_record(self, bill):
|
||||
br = None
|
||||
|
||||
# Note: check for != 0 not > 0, as we allow discounts to be expressed with < 0
|
||||
if self.one_time_price != 0 and self.billrecord_set.count() == 0:
|
||||
br = BillRecord.objects.create(bill=bill,
|
||||
order=self,
|
||||
starting_date=self.starting_date,
|
||||
ending_date=self.starting_date,
|
||||
is_recurring_record=False)
|
||||
|
||||
if self.recurring_price != 0:
|
||||
br = BillRecord.objects.filter(bill=bill, order=self, is_recurring_record=True).first()
|
||||
|
||||
if br:
|
||||
self.update_bill_record_for_recurring_order(br, bill)
|
||||
records = BillRecord.objects.filter(order=self).all()
|
||||
if not records:
|
||||
if self.one_time_price:
|
||||
br = BillRecord.objects.create(bill=bill,
|
||||
order=self,
|
||||
starting_date=self.starting_date,
|
||||
ending_date=bill.ending_date,
|
||||
is_recurring_record=False)
|
||||
else:
|
||||
br = self.create_new_bill_record_for_recurring_order(bill)
|
||||
else:
|
||||
br = self.create_new_bill_record_for_recurring_order(bill)
|
||||
|
||||
opened_recurring_record = BillRecord.objects.filter(bill=bill, order=self, is_recurring_record=True).first()
|
||||
if opened_recurring_record:
|
||||
br = opened_recurring_record
|
||||
self.update_bill_record_for_recurring_order(br, bill)
|
||||
else:
|
||||
br = self.create_new_bill_record_for_recurring_order(bill)
|
||||
return br
|
||||
|
||||
def update_bill_record_for_recurring_order(self,
|
||||
|
|
@ -861,22 +860,21 @@ class Order(models.Model):
|
|||
|
||||
# If the order has an ending date set, we might need to adjust the bill_record
|
||||
if self.ending_date:
|
||||
if bill_record_for_this_bill.ending_date != self.ending_date:
|
||||
bill_record_for_this_bill.ending_date = self.ending_date
|
||||
if bill_record.ending_date != self.ending_date:
|
||||
bill_record.ending_date = self.ending_date
|
||||
|
||||
else:
|
||||
# recurring, not terminated, should go until at least end of bill
|
||||
if bill_record_for_this_bill.ending_date < bill.ending_date:
|
||||
bill_record_for_this_bill.ending_date = bill.ending_date
|
||||
if bill_record.ending_date < bill.ending_date:
|
||||
bill_record.ending_date = bill.ending_date
|
||||
|
||||
bill_record_for_this_bill.save()
|
||||
bill_record.save()
|
||||
|
||||
def create_new_bill_record_for_recurring_order(self, bill):
|
||||
"""
|
||||
Create a new bill record
|
||||
"""
|
||||
|
||||
last_bill_record = BillRecord.objects.filter(order=self, is_recurring_record=True).order_by('id').last()
|
||||
last_bill_record = BillRecord.objects.filter(order=self).order_by('id').last()
|
||||
|
||||
starting_date=self.starting_date
|
||||
|
||||
|
|
@ -892,7 +890,6 @@ class Order(models.Model):
|
|||
return
|
||||
|
||||
starting_date = start_after(last_bill_record.ending_date)
|
||||
|
||||
ending_date = self.get_ending_date_for_bill(bill)
|
||||
|
||||
return BillRecord.objects.create(bill=bill,
|
||||
|
|
@ -901,47 +898,27 @@ class Order(models.Model):
|
|||
ending_date=ending_date,
|
||||
is_recurring_record=True)
|
||||
|
||||
def calculate_prices_and_config(self):
|
||||
one_time_price = 0
|
||||
recurring_price = 0
|
||||
def calculate_recurring_price(self):
|
||||
try:
|
||||
config = json.loads(self.config)
|
||||
recurring_price = 0
|
||||
if 'cores' in config:
|
||||
recurring_price += self.pricing_plan.cores_unit_price * int(config['cores'])
|
||||
if 'memory' in config:
|
||||
recurring_price += self.pricing_plan.ram_unit_price * int(config['memory'])
|
||||
if 'storage' in config:
|
||||
recurring_price += self.pricing_plan.storage_unit_price * int(config['storage'])
|
||||
|
||||
if self.config:
|
||||
config = self.config
|
||||
|
||||
if 'features' not in self.config:
|
||||
self.config['features'] = {}
|
||||
|
||||
else:
|
||||
config = {
|
||||
'features': {}
|
||||
}
|
||||
|
||||
# FIXME: adjust prices to the selected recurring_period to the
|
||||
|
||||
if 'features' in self.product.config:
|
||||
for feature in self.product.config['features']:
|
||||
|
||||
# Set min to 0 if not specified
|
||||
min_val = self.product.config['features'][feature].get('min', 0)
|
||||
|
||||
# We might not even have 'features' cannot use .get() on it
|
||||
try:
|
||||
value = self.config['features'][feature]
|
||||
except (KeyError, TypeError):
|
||||
value = self.product.config['features'][feature]['min']
|
||||
|
||||
# Set max to current value if not specified
|
||||
max_val = self.product.config['features'][feature].get('max', value)
|
||||
|
||||
|
||||
if value < min_val or value > max_val:
|
||||
raise ValidationError(f"Feature '{feature}' must be at least {min_val} and at maximum {max_val}. Value is: {value}")
|
||||
|
||||
one_time_price += self.product.config['features'][feature]['one_time_price_per_unit'] * value
|
||||
recurring_price += self.product.config['features'][feature]['recurring_price_per_unit'] * value
|
||||
config['features'][feature] = value
|
||||
|
||||
return (one_time_price, recurring_price, config)
|
||||
vat_rate = VATRate.get_vat_rate(self.billing_address)
|
||||
vat_validation_status = "verified" if self.billing_address.vat_number_validated_on and self.billing_address.vat_number_verified else False
|
||||
subtotal, subtotal_after_discount, price_after_discount_with_vat, vat, vat_percent, discount = uncloud_pay.utils.apply_vat_discount(
|
||||
recurring_price, self.pricing_plan,
|
||||
vat_rate=vat_rate * 100, vat_validation_status = vat_validation_status
|
||||
)
|
||||
return price_after_discount_with_vat
|
||||
except Exception as e:
|
||||
logger.error("An error occurred while parsing the config obj", e)
|
||||
return 0
|
||||
|
||||
def check_parameters(self):
|
||||
if 'parameters' in self.product.config:
|
||||
|
|
@ -955,7 +932,7 @@ class Order(models.Model):
|
|||
# IMMUTABLE fields -- need to create new order to modify them
|
||||
# However this is not enforced here...
|
||||
if self._state.adding:
|
||||
(self.one_time_price, self.recurring_price, self.config) = self.calculate_prices_and_config()
|
||||
self.recurring_price = self.calculate_recurring_price()
|
||||
|
||||
if self.recurring_period_id is None:
|
||||
self.recurring_period = self.product.default_recurring_period
|
||||
|
|
@ -975,12 +952,7 @@ class Order(models.Model):
|
|||
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
conf = " ".join([ f"{key}:{val}" for key,val in self.config['features'].items() if val != 0 ])
|
||||
except KeyError:
|
||||
conf = ""
|
||||
|
||||
return f"Order {self.id}: {self.description} {conf}"
|
||||
return f"Order {self.id}: {self.description}"
|
||||
|
||||
class Bill(models.Model):
|
||||
"""
|
||||
|
|
@ -1003,7 +975,7 @@ class Bill(models.Model):
|
|||
# FIXME: editable=True -> is in the admin, but also editable in DRF
|
||||
# Maybe filter fields in the serializer?
|
||||
|
||||
is_final = models.BooleanField(default=False)
|
||||
is_closed = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
|
|
@ -1017,8 +989,9 @@ class Bill(models.Model):
|
|||
"""
|
||||
Close/finish a bill
|
||||
"""
|
||||
|
||||
self.is_final = True
|
||||
self.is_closed = True
|
||||
if not self.ending_date:
|
||||
self.ending_date = timezone.now()
|
||||
self.save()
|
||||
|
||||
@property
|
||||
|
|
@ -1028,34 +1001,7 @@ class Bill(models.Model):
|
|||
|
||||
@property
|
||||
def vat_rate(self):
|
||||
"""
|
||||
Handling VAT is a tricky business - thus we only implement the cases
|
||||
that we clearly now and leave it open to fellow developers to implement
|
||||
correct handling for other cases.
|
||||
|
||||
Case CH:
|
||||
|
||||
- If the customer is in .ch -> apply standard rate
|
||||
- If the customer is in EU AND private -> apply country specific rate
|
||||
- If the customer is in EU AND business -> do not apply VAT
|
||||
- If the customer is outside EU and outside CH -> do not apply VAT
|
||||
"""
|
||||
|
||||
provider = UncloudProvider.objects.get()
|
||||
|
||||
# Assume always VAT inside the country
|
||||
if provider.country == self.billing_address.country:
|
||||
vat_rate = VATRate.objects.get(country=provider.country,
|
||||
when=self.ending_date)
|
||||
elif self.billing_address.country in EU:
|
||||
# FIXME: need to check for validated vat number
|
||||
if self.billing_address.vat_number:
|
||||
return 0
|
||||
else:
|
||||
return VATRate.objects.get(country=self.biling_address.country,
|
||||
when=self.ending_date)
|
||||
else: # non-EU, non-national
|
||||
return 0
|
||||
return VATRate.get_vat_rate(self.billing_address, when=self.ending_date)
|
||||
|
||||
|
||||
@classmethod
|
||||
|
|
@ -1075,9 +1021,10 @@ class Bill(models.Model):
|
|||
"""
|
||||
|
||||
bills = []
|
||||
|
||||
for billing_address in BillingAddress.objects.filter(owner=owner):
|
||||
bills.append(cls.create_next_bill_for_user_address(billing_address, ending_date))
|
||||
bill = cls.create_next_bill_for_user_address(billing_address, ending_date)
|
||||
if bill:
|
||||
bills.append(bill)
|
||||
|
||||
return bills
|
||||
|
||||
|
|
@ -1089,15 +1036,18 @@ class Bill(models.Model):
|
|||
|
||||
owner = billing_address.owner
|
||||
|
||||
all_orders = Order.objects.filter(owner=owner,
|
||||
billing_address=billing_address).order_by('id')
|
||||
|
||||
bill = cls.get_or_create_bill(billing_address, ending_date=ending_date)
|
||||
|
||||
for order in all_orders:
|
||||
order.create_bill_record(bill)
|
||||
|
||||
return bill
|
||||
all_orders = Order.objects.filter(Q(owner__id=owner.id), Q(should_be_billed=True),
|
||||
Q(billing_address__id=billing_address.id)
|
||||
).order_by('id')
|
||||
|
||||
if len(all_orders) > 0:
|
||||
bill = cls.get_or_create_bill(billing_address, ending_date=ending_date)
|
||||
for order in all_orders:
|
||||
order.create_bill_record(bill)
|
||||
return bill
|
||||
else:
|
||||
# This Customer Hasn't any active orders
|
||||
return False
|
||||
|
||||
|
||||
@classmethod
|
||||
|
|
@ -1117,7 +1067,7 @@ class Bill(models.Model):
|
|||
|
||||
# Get date & bill from previous bill, if it exists
|
||||
if last_bill:
|
||||
if not last_bill.is_final:
|
||||
if not last_bill.is_closed:
|
||||
bill = last_bill
|
||||
starting_date = last_bill.starting_date
|
||||
ending_date = bill.ending_date
|
||||
|
|
@ -1142,7 +1092,7 @@ class Bill(models.Model):
|
|||
|
||||
|
||||
return bill
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.owner}-{self.id}"
|
||||
|
||||
|
|
@ -1167,9 +1117,11 @@ class BillRecord(models.Model):
|
|||
if not self.is_recurring_record:
|
||||
return 1
|
||||
|
||||
record_delta = self.ending_date - self.starting_date
|
||||
|
||||
return record_delta.total_seconds()/self.order.recurring_period.duration_seconds
|
||||
record_delta = self.ending_date.date() - self.starting_date.date()
|
||||
if self.order.recurring_period and self.order.recurring_period.duration_seconds > 0:
|
||||
return int(record_delta.total_seconds() / self.order.recurring_period.duration_seconds)
|
||||
else:
|
||||
return 1
|
||||
|
||||
@property
|
||||
def sum(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue