- 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 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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
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:
|
||||
handler.roll(active_component, self.close_date, update_payload={
|
||||
"invoice": new_invoice
|
||||
})
|
||||
}, fallback_price=True)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ INVOICE_TAX = "invoice_tax"
|
|||
|
||||
DEFAULTS = {
|
||||
BILLING_ENABLED: False,
|
||||
INVOICE_TAX: 10
|
||||
INVOICE_TAX: 11
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue