- 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 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()

View file

@ -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)

View file

@ -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):

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
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: 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)

View file

@ -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()

View file

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