- Handle unknown price
- Fix Tax - Handle price not found
This commit is contained in:
parent
f865ef8c99
commit
2222c0c042
14 changed files with 116 additions and 18 deletions
33
api/views.py
33
api/views.py
|
@ -9,6 +9,7 @@ from rest_framework.response import Response
|
||||||
from api.serializers import InvoiceSerializer, SimpleInvoiceSerializer
|
from api.serializers import InvoiceSerializer, SimpleInvoiceSerializer
|
||||||
from core.component import component
|
from core.component import component
|
||||||
from core.component.component import INVOICE_COMPONENT_MODEL
|
from core.component.component import INVOICE_COMPONENT_MODEL
|
||||||
|
from core.exception import PriceNotFound
|
||||||
from core.models import Invoice, BillingProject
|
from core.models import Invoice, BillingProject
|
||||||
from core.utils.dynamic_setting import get_dynamic_settings, get_dynamic_setting, set_dynamic_setting, BILLING_ENABLED
|
from core.utils.dynamic_setting import get_dynamic_settings, get_dynamic_setting, set_dynamic_setting, BILLING_ENABLED
|
||||||
|
|
||||||
|
@ -74,9 +75,26 @@ class InvoiceViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
@action(detail=False, methods=['POST'])
|
@action(detail=False, methods=['POST'])
|
||||||
def enable_billing(self, request):
|
def enable_billing(self, request):
|
||||||
# TODO: Handle unknown price
|
try:
|
||||||
self.handle_init_billing(request.data)
|
self.handle_init_billing(request.data)
|
||||||
|
return Response({
|
||||||
|
"status": "success"
|
||||||
|
})
|
||||||
|
except PriceNotFound as e:
|
||||||
|
return Response({
|
||||||
|
"message": str(e.identifier) + " price not found. Please check price configuration"
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['POST'])
|
||||||
|
def disable_billing(self, request):
|
||||||
|
set_dynamic_setting(BILLING_ENABLED, False)
|
||||||
|
return Response({
|
||||||
|
"status": "success"
|
||||||
|
})
|
||||||
|
|
||||||
|
@action(detail=False, methods=['POST'])
|
||||||
|
def reset_billing(self, request):
|
||||||
|
self.handle_reset_billing()
|
||||||
return Response({
|
return Response({
|
||||||
"status": "success"
|
"status": "success"
|
||||||
})
|
})
|
||||||
|
@ -119,6 +137,17 @@ class InvoiceViewSet(viewsets.ModelViewSet):
|
||||||
del payload['tenant_id']
|
del payload['tenant_id']
|
||||||
handler.create(payload)
|
handler.create(payload)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def handle_reset_billing(self):
|
||||||
|
set_dynamic_setting(BILLING_ENABLED, False)
|
||||||
|
|
||||||
|
BillingProject.objects.all().delete()
|
||||||
|
for name, handler in component.INVOICE_HANDLER.items():
|
||||||
|
handler.delete()
|
||||||
|
|
||||||
|
for name, model in component.PRICE_MODEL.items():
|
||||||
|
model.objects.all().delete()
|
||||||
|
|
||||||
@action(detail=True)
|
@action(detail=True)
|
||||||
def finish(self, request, pk):
|
def finish(self, request, pk):
|
||||||
invoice = Invoice.objects.filter(id=pk).first()
|
invoice = Invoice.objects.filter(id=pk).first()
|
||||||
|
|
|
@ -46,7 +46,7 @@ class BillingProjectAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(Invoice)
|
@admin.register(Invoice)
|
||||||
class InvoiceAdmin(admin.ModelAdmin):
|
class InvoiceAdmin(admin.ModelAdmin):
|
||||||
list_display = ('__str__', 'project',)
|
list_display = ('__str__', 'project', 'start_date')
|
||||||
|
|
||||||
|
|
||||||
@admin.register(InvoiceInstance)
|
@admin.register(InvoiceInstance)
|
||||||
|
|
|
@ -70,7 +70,7 @@ class EventHandler(metaclass=abc.ABCMeta):
|
||||||
payload['invoice'] = invoice
|
payload['invoice'] = invoice
|
||||||
payload['start_date'] = timezone.now()
|
payload['start_date'] = timezone.now()
|
||||||
|
|
||||||
self.invoice_handler.create(payload)
|
self.invoice_handler.create(payload, fallback_price=True)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ class EventHandler(metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
if instance:
|
if instance:
|
||||||
if self.invoice_handler.is_price_dependency_changed(instance, payload):
|
if self.invoice_handler.is_price_dependency_changed(instance, payload):
|
||||||
self.invoice_handler.roll(instance, close_date=timezone.now(), update_payload=payload)
|
self.invoice_handler.roll(instance, close_date=timezone.now(), update_payload=payload, fallback_price=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if self.invoice_handler.is_informative_changed(instance, payload):
|
if self.invoice_handler.is_informative_changed(instance, payload):
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import abc
|
import abc
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from djmoney.money import Money
|
||||||
|
|
||||||
|
from core.exception import PriceNotFound
|
||||||
from core.models import InvoiceComponentMixin, PriceMixin
|
from core.models import InvoiceComponentMixin, PriceMixin
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,25 +14,41 @@ class InvoiceHandler(metaclass=abc.ABCMeta):
|
||||||
INFORMATIVE_FIELDS = []
|
INFORMATIVE_FIELDS = []
|
||||||
PRICE_DEPENDENCY_FIELDS = []
|
PRICE_DEPENDENCY_FIELDS = []
|
||||||
|
|
||||||
def create(self, payload):
|
def create(self, payload, fallback_price=False):
|
||||||
"""
|
"""
|
||||||
Create new invoice component
|
Create new invoice component
|
||||||
:param payload: the data that will be created
|
:param payload: the data that will be created
|
||||||
|
:param fallback_price: Whether use 0 price if price not found
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
price = self.get_price(payload)
|
try:
|
||||||
|
price = self.get_price(payload)
|
||||||
|
if price is None:
|
||||||
|
raise PriceNotFound()
|
||||||
|
except PriceNotFound:
|
||||||
|
if fallback_price:
|
||||||
|
price = PriceMixin()
|
||||||
|
price.hourly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
|
||||||
|
price.monthly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
payload['hourly_price'] = price.hourly_price
|
payload['hourly_price'] = price.hourly_price
|
||||||
payload['monthly_price'] = price.monthly_price
|
payload['monthly_price'] = price.monthly_price
|
||||||
|
|
||||||
self.INVOICE_CLASS.objects.create(**payload)
|
self.INVOICE_CLASS.objects.create(**payload)
|
||||||
|
|
||||||
def roll(self, instance: InvoiceComponentMixin, close_date, update_payload=None):
|
def delete(self):
|
||||||
|
self.INVOICE_CLASS.objects.all().delete()
|
||||||
|
|
||||||
|
def roll(self, instance: InvoiceComponentMixin, close_date, update_payload=None, fallback_price=False):
|
||||||
"""
|
"""
|
||||||
Roll current instance.
|
Roll current instance.
|
||||||
Close current component instance and clone it
|
Close current component instance and clone it
|
||||||
:param instance: The instance that want to be rolled
|
:param instance: The instance that want to be rolled
|
||||||
:param close_date: The close date of current instance
|
:param close_date: The close date of current instance
|
||||||
:param update_payload: New data to update the next component instance
|
:param update_payload: New data to update the next component instance
|
||||||
|
:param fallback_price: Whether use 0 price if price not found
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if update_payload is None:
|
if update_payload is None:
|
||||||
|
@ -51,7 +70,17 @@ class InvoiceHandler(metaclass=abc.ABCMeta):
|
||||||
instance = self.update(instance, update_payload, save=False)
|
instance = self.update(instance, update_payload, save=False)
|
||||||
|
|
||||||
# Update the price
|
# Update the price
|
||||||
price = self.get_price(self.get_price_dependency_from_instance(instance))
|
try:
|
||||||
|
price = self.get_price(self.get_price_dependency_from_instance(instance))
|
||||||
|
if price is None:
|
||||||
|
raise PriceNotFound()
|
||||||
|
except PriceNotFound:
|
||||||
|
if fallback_price:
|
||||||
|
price = PriceMixin()
|
||||||
|
price.hourly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
|
||||||
|
price.monthly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
instance.hourly_price = price.hourly_price
|
instance.hourly_price = price.hourly_price
|
||||||
instance.monthly_price = price.monthly_price
|
instance.monthly_price = price.monthly_price
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from core.exception import PriceNotFound
|
||||||
from core.models import FloatingIpsPrice, InvoiceFloatingIp, PriceMixin
|
from core.models import FloatingIpsPrice, InvoiceFloatingIp, PriceMixin
|
||||||
from core.component.base.invoice_handler import InvoiceHandler
|
from core.component.base.invoice_handler import InvoiceHandler
|
||||||
|
|
||||||
|
@ -9,4 +10,10 @@ class FloatingIpInvoiceHandler(InvoiceHandler):
|
||||||
INFORMATIVE_FIELDS = ["ip"]
|
INFORMATIVE_FIELDS = ["ip"]
|
||||||
|
|
||||||
def get_price(self, payload) -> PriceMixin:
|
def get_price(self, payload) -> PriceMixin:
|
||||||
return FloatingIpsPrice.objects.first()
|
price = FloatingIpsPrice.objects.first()
|
||||||
|
|
||||||
|
if price is None:
|
||||||
|
raise PriceNotFound(identifier='floating ip')
|
||||||
|
|
||||||
|
return price
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from core.component.base.invoice_handler import InvoiceHandler
|
from core.component.base.invoice_handler import InvoiceHandler
|
||||||
|
from core.exception import PriceNotFound
|
||||||
from core.models import PriceMixin, InvoiceImage, ImagePrice
|
from core.models import PriceMixin, InvoiceImage, ImagePrice
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,4 +10,8 @@ class ImageInvoiceHandler(InvoiceHandler):
|
||||||
INFORMATIVE_FIELDS = ["name"]
|
INFORMATIVE_FIELDS = ["name"]
|
||||||
|
|
||||||
def get_price(self, payload) -> PriceMixin:
|
def get_price(self, payload) -> PriceMixin:
|
||||||
return ImagePrice.objects.first()
|
price = ImagePrice.objects.first()
|
||||||
|
if price is None:
|
||||||
|
raise PriceNotFound(identifier='image')
|
||||||
|
|
||||||
|
return price
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from core.exception import PriceNotFound
|
||||||
from core.models import FlavorPrice, InvoiceInstance, PriceMixin
|
from core.models import FlavorPrice, InvoiceInstance, PriceMixin
|
||||||
from core.component.base.invoice_handler import InvoiceHandler
|
from core.component.base.invoice_handler import InvoiceHandler
|
||||||
|
|
||||||
|
@ -9,4 +10,9 @@ class InstanceInvoiceHandler(InvoiceHandler):
|
||||||
INFORMATIVE_FIELDS = ['name']
|
INFORMATIVE_FIELDS = ['name']
|
||||||
|
|
||||||
def get_price(self, payload) -> PriceMixin:
|
def get_price(self, payload) -> PriceMixin:
|
||||||
return FlavorPrice.objects.filter(flavor_id=payload['flavor_id']).first()
|
price = FlavorPrice.objects.filter(flavor_id=payload['flavor_id']).first()
|
||||||
|
|
||||||
|
if price is None:
|
||||||
|
raise PriceNotFound(identifier='flavor')
|
||||||
|
|
||||||
|
return price
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from core.component.base.invoice_handler import InvoiceHandler
|
from core.component.base.invoice_handler import InvoiceHandler
|
||||||
|
from core.exception import PriceNotFound
|
||||||
from core.models import PriceMixin, InvoiceRouter, RouterPrice
|
from core.models import PriceMixin, InvoiceRouter, RouterPrice
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,4 +10,8 @@ class RouterInvoiceHandler(InvoiceHandler):
|
||||||
INFORMATIVE_FIELDS = ["name"]
|
INFORMATIVE_FIELDS = ["name"]
|
||||||
|
|
||||||
def get_price(self, payload) -> PriceMixin:
|
def get_price(self, payload) -> PriceMixin:
|
||||||
return RouterPrice.objects.first()
|
price = RouterPrice.objects.first()
|
||||||
|
if price is None:
|
||||||
|
raise PriceNotFound(identifier='router')
|
||||||
|
|
||||||
|
return price
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from core.component.base.invoice_handler import InvoiceHandler
|
from core.component.base.invoice_handler import InvoiceHandler
|
||||||
|
from core.exception import PriceNotFound
|
||||||
from core.models import PriceMixin, InvoiceSnapshot, SnapshotPrice
|
from core.models import PriceMixin, InvoiceSnapshot, SnapshotPrice
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,4 +10,8 @@ class SnapshotInvoiceHandler(InvoiceHandler):
|
||||||
INFORMATIVE_FIELDS = ["name"]
|
INFORMATIVE_FIELDS = ["name"]
|
||||||
|
|
||||||
def get_price(self, payload) -> PriceMixin:
|
def get_price(self, payload) -> PriceMixin:
|
||||||
return SnapshotPrice.objects.first()
|
price = SnapshotPrice.objects.first()
|
||||||
|
if price is None:
|
||||||
|
raise PriceNotFound(identifier='snapshot')
|
||||||
|
|
||||||
|
return price
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from core.exception import PriceNotFound
|
||||||
from core.models import VolumePrice, InvoiceVolume, PriceMixin
|
from core.models import VolumePrice, InvoiceVolume, PriceMixin
|
||||||
from core.component.base.invoice_handler import InvoiceHandler
|
from core.component.base.invoice_handler import InvoiceHandler
|
||||||
|
|
||||||
|
@ -9,4 +10,9 @@ class VolumeInvoiceHandler(InvoiceHandler):
|
||||||
INFORMATIVE_FIELDS = ['volume_name']
|
INFORMATIVE_FIELDS = ['volume_name']
|
||||||
|
|
||||||
def get_price(self, payload) -> PriceMixin:
|
def get_price(self, payload) -> PriceMixin:
|
||||||
return VolumePrice.objects.filter(volume_type_id=payload['volume_type_id']).first()
|
price = VolumePrice.objects.filter(volume_type_id=payload['volume_type_id']).first()
|
||||||
|
|
||||||
|
if price is None:
|
||||||
|
raise PriceNotFound(identifier='volume')
|
||||||
|
|
||||||
|
return price
|
||||||
|
|
3
core/exception.py
Normal file
3
core/exception.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
class PriceNotFound(Exception):
|
||||||
|
def __init__(self, identifier=None):
|
||||||
|
self.identifier = identifier
|
|
@ -54,4 +54,4 @@ class Command(BaseCommand):
|
||||||
for active_component in active_components:
|
for active_component in active_components:
|
||||||
handler.roll(active_component, self.close_date, update_payload={
|
handler.roll(active_component, self.close_date, update_payload={
|
||||||
"invoice": new_invoice
|
"invoice": new_invoice
|
||||||
})
|
}, fallback_price=True)
|
||||||
|
|
|
@ -60,6 +60,9 @@ class ImagePrice(BaseModel, TimestampMixin, PriceMixin):
|
||||||
class BillingProject(BaseModel, TimestampMixin):
|
class BillingProject(BaseModel, TimestampMixin):
|
||||||
tenant_id = models.CharField(max_length=256)
|
tenant_id = models.CharField(max_length=256)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.tenant_id
|
||||||
|
|
||||||
|
|
||||||
class Invoice(BaseModel, TimestampMixin):
|
class Invoice(BaseModel, TimestampMixin):
|
||||||
class InvoiceState(models.IntegerChoices):
|
class InvoiceState(models.IntegerChoices):
|
||||||
|
@ -102,7 +105,7 @@ class Invoice(BaseModel, TimestampMixin):
|
||||||
def close(self, date, tax_percentage):
|
def close(self, date, tax_percentage):
|
||||||
self.state = Invoice.InvoiceState.UNPAID
|
self.state = Invoice.InvoiceState.UNPAID
|
||||||
self.end_date = date
|
self.end_date = date
|
||||||
self.tax = tax_percentage * self.subtotal
|
self.tax = tax_percentage * self.subtotal / 100
|
||||||
self.total = self.tax + self.subtotal
|
self.total = self.tax + self.subtotal
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ INVOICE_TAX = "invoice_tax"
|
||||||
|
|
||||||
DEFAULTS = {
|
DEFAULTS = {
|
||||||
BILLING_ENABLED: False,
|
BILLING_ENABLED: False,
|
||||||
INVOICE_TAX: 10
|
INVOICE_TAX: 11
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue