- Handle unknown price

- Fix Tax
- Handle price not found
This commit is contained in:
Setyo Nugroho 2022-04-22 02:09:02 +07:00
parent f865ef8c99
commit 2222c0c042
14 changed files with 116 additions and 18 deletions

View file

@ -9,6 +9,7 @@ from rest_framework.response import Response
from api.serializers import InvoiceSerializer, SimpleInvoiceSerializer
from core.component import component
from core.component.component import INVOICE_COMPONENT_MODEL
from core.exception import PriceNotFound
from core.models import Invoice, BillingProject
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'])
def enable_billing(self, request):
# TODO: Handle unknown price
self.handle_init_billing(request.data)
try:
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({
"status": "success"
})
@ -119,6 +137,17 @@ class InvoiceViewSet(viewsets.ModelViewSet):
del payload['tenant_id']
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)
def finish(self, request, pk):
invoice = Invoice.objects.filter(id=pk).first()

View file

@ -46,7 +46,7 @@ class BillingProjectAdmin(admin.ModelAdmin):
@admin.register(Invoice)
class InvoiceAdmin(admin.ModelAdmin):
list_display = ('__str__', 'project',)
list_display = ('__str__', 'project', 'start_date')
@admin.register(InvoiceInstance)

View file

@ -70,7 +70,7 @@ class EventHandler(metaclass=abc.ABCMeta):
payload['invoice'] = invoice
payload['start_date'] = timezone.now()
self.invoice_handler.create(payload)
self.invoice_handler.create(payload, fallback_price=True)
return True
@ -113,7 +113,7 @@ class EventHandler(metaclass=abc.ABCMeta):
if instance:
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
if self.invoice_handler.is_informative_changed(instance, payload):

View file

@ -1,7 +1,10 @@
import abc
from django.conf import settings
from django.utils import timezone
from djmoney.money import Money
from core.exception import PriceNotFound
from core.models import InvoiceComponentMixin, PriceMixin
@ -11,25 +14,41 @@ class InvoiceHandler(metaclass=abc.ABCMeta):
INFORMATIVE_FIELDS = []
PRICE_DEPENDENCY_FIELDS = []
def create(self, payload):
def create(self, payload, fallback_price=False):
"""
Create new invoice component
:param payload: the data that will be created
:param fallback_price: Whether use 0 price if price not found
: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['monthly_price'] = price.monthly_price
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.
Close current component instance and clone it
:param instance: The instance that want to be rolled
:param close_date: The close date of current instance
:param update_payload: New data to update the next component instance
:param fallback_price: Whether use 0 price if price not found
:return:
"""
if update_payload is None:
@ -51,7 +70,17 @@ class InvoiceHandler(metaclass=abc.ABCMeta):
instance = self.update(instance, update_payload, save=False)
# 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.monthly_price = price.monthly_price
instance.save()

View file

@ -1,3 +1,4 @@
from core.exception import PriceNotFound
from core.models import FloatingIpsPrice, InvoiceFloatingIp, PriceMixin
from core.component.base.invoice_handler import InvoiceHandler
@ -9,4 +10,10 @@ class FloatingIpInvoiceHandler(InvoiceHandler):
INFORMATIVE_FIELDS = ["ip"]
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

View file

@ -1,4 +1,5 @@
from core.component.base.invoice_handler import InvoiceHandler
from core.exception import PriceNotFound
from core.models import PriceMixin, InvoiceImage, ImagePrice
@ -9,4 +10,8 @@ class ImageInvoiceHandler(InvoiceHandler):
INFORMATIVE_FIELDS = ["name"]
def get_price(self, payload) -> PriceMixin:
return ImagePrice.objects.first()
price = ImagePrice.objects.first()
if price is None:
raise PriceNotFound(identifier='image')
return price

View file

@ -1,3 +1,4 @@
from core.exception import PriceNotFound
from core.models import FlavorPrice, InvoiceInstance, PriceMixin
from core.component.base.invoice_handler import InvoiceHandler
@ -9,4 +10,9 @@ class InstanceInvoiceHandler(InvoiceHandler):
INFORMATIVE_FIELDS = ['name']
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

View file

@ -1,4 +1,5 @@
from core.component.base.invoice_handler import InvoiceHandler
from core.exception import PriceNotFound
from core.models import PriceMixin, InvoiceRouter, RouterPrice
@ -9,4 +10,8 @@ class RouterInvoiceHandler(InvoiceHandler):
INFORMATIVE_FIELDS = ["name"]
def get_price(self, payload) -> PriceMixin:
return RouterPrice.objects.first()
price = RouterPrice.objects.first()
if price is None:
raise PriceNotFound(identifier='router')
return price

View file

@ -1,4 +1,5 @@
from core.component.base.invoice_handler import InvoiceHandler
from core.exception import PriceNotFound
from core.models import PriceMixin, InvoiceSnapshot, SnapshotPrice
@ -9,4 +10,8 @@ class SnapshotInvoiceHandler(InvoiceHandler):
INFORMATIVE_FIELDS = ["name"]
def get_price(self, payload) -> PriceMixin:
return SnapshotPrice.objects.first()
price = SnapshotPrice.objects.first()
if price is None:
raise PriceNotFound(identifier='snapshot')
return price

View file

@ -1,3 +1,4 @@
from core.exception import PriceNotFound
from core.models import VolumePrice, InvoiceVolume, PriceMixin
from core.component.base.invoice_handler import InvoiceHandler
@ -9,4 +10,9 @@ class VolumeInvoiceHandler(InvoiceHandler):
INFORMATIVE_FIELDS = ['volume_name']
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
View file

@ -0,0 +1,3 @@
class PriceNotFound(Exception):
def __init__(self, identifier=None):
self.identifier = identifier

View file

@ -54,4 +54,4 @@ class Command(BaseCommand):
for active_component in active_components:
handler.roll(active_component, self.close_date, update_payload={
"invoice": new_invoice
})
}, fallback_price=True)

View file

@ -60,6 +60,9 @@ class ImagePrice(BaseModel, TimestampMixin, PriceMixin):
class BillingProject(BaseModel, TimestampMixin):
tenant_id = models.CharField(max_length=256)
def __str__(self):
return self.tenant_id
class Invoice(BaseModel, TimestampMixin):
class InvoiceState(models.IntegerChoices):
@ -102,7 +105,7 @@ class Invoice(BaseModel, TimestampMixin):
def close(self, date, tax_percentage):
self.state = Invoice.InvoiceState.UNPAID
self.end_date = date
self.tax = tax_percentage * self.subtotal
self.tax = tax_percentage * self.subtotal / 100
self.total = self.tax + self.subtotal
self.save()

View file

@ -7,7 +7,7 @@ INVOICE_TAX = "invoice_tax"
DEFAULTS = {
BILLING_ENABLED: False,
INVOICE_TAX: 10
INVOICE_TAX: 11
}