Merge branch 'mega_refactor' into 'master'

Refactoring and Updating Component

See merge request dev/rintik!1
This commit is contained in:
Setyo Nugroho 2021-10-29 13:00:33 +07:00
commit b7088f9844
51 changed files with 1332 additions and 992 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View file

@ -1,65 +1,42 @@
from djmoney.contrib.django_rest_framework import MoneyField
from rest_framework import serializers
from core.models import FlavorPrice, VolumePrice, FloatingIpsPrice, Invoice, InvoiceInstance, InvoiceVolume, \
InvoiceFloatingIp
from core.models import Invoice
from core.component import component
class FlavorPriceSerializer(serializers.ModelSerializer):
class Meta:
model = FlavorPrice
fields = '__all__'
class FloatingIpsPriceSerializer(serializers.ModelSerializer):
class Meta:
model = FloatingIpsPrice
fields = '__all__'
class VolumePriceSerializer(serializers.ModelSerializer):
class Meta:
model = VolumePrice
fields = '__all__'
class InvoiceInstanceSerializer(serializers.ModelSerializer):
class InvoiceComponentSerializer(serializers.ModelSerializer):
adjusted_end_date = serializers.DateTimeField()
price_charged = MoneyField(max_digits=10, decimal_places=0)
price_charged_currency = serializers.CharField(source="price_charged.currency")
class Meta:
model = InvoiceInstance
fields = '__all__'
def generate_invoice_component_serializer(model):
"""
Generate Invoice Component Serializer for particular model
:param model: The invoice component model
:return: serializer for particular model
"""
name = type(model).__name__
meta_params = {
"model": model,
"fields": "__all__"
}
meta_class = type("Meta", (object,), meta_params)
serializer_class = type(f"{name}Serializer", (InvoiceComponentSerializer,), {"Meta": meta_class})
class InvoiceVolumeSerializer(serializers.ModelSerializer):
adjusted_end_date = serializers.DateTimeField()
price_charged = MoneyField(max_digits=10, decimal_places=0)
price_charged_currency = serializers.CharField(source="price_charged.currency")
class Meta:
model = InvoiceVolume
fields = '__all__'
class InvoiceFloatingIpSerializer(serializers.ModelSerializer):
adjusted_end_date = serializers.DateTimeField()
price_charged = MoneyField(max_digits=10, decimal_places=0)
price_charged_currency = serializers.CharField(source="price_charged.currency")
class Meta:
model = InvoiceFloatingIp
fields = '__all__'
return serializer_class
class InvoiceSerializer(serializers.ModelSerializer):
instances = InvoiceInstanceSerializer(many=True)
floating_ips = InvoiceFloatingIpSerializer(many=True)
volumes = InvoiceVolumeSerializer(many=True)
subtotal = MoneyField(max_digits=10, decimal_places=0)
subtotal_currency = serializers.CharField(source="subtotal.currency")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field, model in component.INVOICE_COMPONENT_MODEL.items():
self.fields[field] = generate_invoice_component_serializer(model)(many=True)
class Meta:
model = Invoice
fields = '__all__'
@ -69,4 +46,3 @@ class SimpleInvoiceSerializer(serializers.ModelSerializer):
class Meta:
model = Invoice
fields = ['id', 'start_date', 'end_date', 'state']

View file

@ -2,11 +2,13 @@ from django.urls import path, include
from rest_framework import routers
from api import views
from core.component import component
router = routers.DefaultRouter()
router.register(r'flavor_price', views.FlavorPriceViewSet)
router.register(r'floating_ips_price', views.FloatingIpsPriceViewSet)
router.register(r'volume_price', views.VolumePriceViewSet)
for name, model in component.PRICE_MODEL.items():
router.register(f"price/{name}", views.get_generic_model_view_set(model))
router.register(r'settings', views.DynamicSettingViewSet, basename='settings')
router.register(r'invoice', views.InvoiceViewSet, basename='invoice')
urlpatterns = [

View file

@ -1,29 +1,55 @@
from django.utils import timezone
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
import dateutil.parser
import pytz
from django.db import transaction
from django.utils import timezone
from rest_framework import viewsets, serializers
from rest_framework.decorators import action
from rest_framework.response import Response
from api.serializers import FlavorPriceSerializer, FloatingIpsPriceSerializer, VolumePriceSerializer, InvoiceSerializer, \
SimpleInvoiceSerializer
from core.models import FlavorPrice, FloatingIpsPrice, VolumePrice, Invoice, BillingProject, InvoiceInstance, \
InvoiceFloatingIp, InvoiceVolume
from api.serializers import InvoiceSerializer, SimpleInvoiceSerializer
from core.models import Invoice, BillingProject
from core.component import component
from core.utils.dynamic_setting import get_dynamic_settings, get_dynamic_setting, set_dynamic_setting, BILLING_ENABLED
class FlavorPriceViewSet(viewsets.ModelViewSet):
queryset = FlavorPrice.objects
serializer_class = FlavorPriceSerializer
def get_generic_model_view_set(model):
name = type(model).__name__
meta_params = {
"model": model,
"fields": "__all__"
}
meta_class = type("Meta", (object,), meta_params)
serializer_class = type(f"{name}Serializer", (serializers.ModelSerializer,), {"Meta": meta_class})
view_set_params = {
"model": model,
"queryset": model.objects,
"serializer_class": serializer_class
}
return type(f"{name}ViewSet", (viewsets.ModelViewSet,), view_set_params)
class FloatingIpsPriceViewSet(viewsets.ModelViewSet):
queryset = FloatingIpsPrice.objects
serializer_class = FloatingIpsPriceSerializer
class DynamicSettingViewSet(viewsets.ViewSet):
def list(self, request):
return Response(get_dynamic_settings())
def retrieve(self, request, pk=None):
return Response({
pk: get_dynamic_setting(pk)
})
class VolumePriceViewSet(viewsets.ModelViewSet):
queryset = VolumePrice.objects
serializer_class = VolumePriceSerializer
def update(self, request, pk=None):
set_dynamic_setting(pk, request.data['value'])
return Response({
pk: get_dynamic_setting(pk)
})
def partial_update(self, request, pk=None):
set_dynamic_setting(pk, request.data['value'])
return Response({
pk: get_dynamic_setting(pk)
})
class InvoiceViewSet(viewsets.ModelViewSet):
@ -46,79 +72,48 @@ class InvoiceViewSet(viewsets.ModelViewSet):
return Response(serializer.data)
@action(detail=False, methods=['POST'])
def init_invoice(self, request):
project, created = BillingProject.objects.get_or_create(tenant_id=request.data['tenant_id'])
def enable_billing(self, request):
# TODO: Handle unknown price
self.handle_init_billing(request.data)
return Response({
"status": "success"
})
@transaction.atomic
def handle_init_billing(self, data):
set_dynamic_setting(BILLING_ENABLED, True)
projects = {}
invoices = {}
date_today = timezone.now()
month_first_day = date_today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
new_invoice = Invoice.objects.create(
project=project,
start_date=month_first_day,
state=Invoice.InvoiceState.IN_PROGRESS
)
new_invoice.save()
for name, handler in component.INVOICE_HANDLER.items():
payloads = data[name]
# Create Instance
for instance in request.data['instances']:
# Get Price
flavor_price = FlavorPrice.objects.filter(flavor_id=instance['flavor_id']).first()
for payload in payloads:
# Create new invoice instance
start_date = self.parse_time(instance['start_date'])
if start_date < month_first_day:
start_date = month_first_day
InvoiceInstance.objects.create(
invoice=new_invoice,
instance_id=instance['instance_id'],
name=instance['name'],
flavor_id=instance['flavor_id'],
current_state=instance['current_state'],
start_date=start_date,
daily_price=flavor_price.daily_price,
monthly_price=flavor_price.monthly_price,
)
if payload['tenant_id'] not in projects:
project, created = BillingProject.objects.get_or_create(tenant_id=payload['tenant_id'])
projects[payload['tenant_id']] = project
for fip in request.data['floating_ips']:
# Get Price
fip_price = FloatingIpsPrice.objects.first()
if payload['tenant_id'] not in invoices:
invoice = Invoice.objects.create(
project=projects[payload['tenant_id']],
start_date=month_first_day,
state=Invoice.InvoiceState.IN_PROGRESS
)
invoices[payload['tenant_id']] = invoice
# Create new invoice floating ip
start_date = self.parse_time(fip['start_date'])
if start_date < month_first_day:
start_date = month_first_day
start_date = self.parse_time(payload['start_date'])
if start_date < month_first_day:
start_date = month_first_day
InvoiceFloatingIp.objects.create(
invoice=new_invoice,
fip_id=fip['fip_id'],
ip=fip['ip'],
current_state=fip['current_state'],
start_date=start_date,
daily_price=fip_price.daily_price,
monthly_price=fip_price.monthly_price,
)
payload['start_date'] = start_date
payload['invoice'] = invoices[payload['tenant_id']]
for volume in request.data['volumes']:
# Get Price
volume_price = VolumePrice.objects.filter(volume_type_id=volume['volume_type_id']).first()
# Create new invoice floating ip
start_date = self.parse_time(volume['start_date'])
if start_date < month_first_day:
start_date = month_first_day
InvoiceVolume.objects.create(
invoice=new_invoice,
volume_id=volume['volume_id'],
volume_name=volume['volume_name'],
volume_type_id=volume['volume_type_id'],
space_allocation_gb=volume['space_allocation_gb'],
current_state=volume['current_state'],
start_date=start_date,
daily_price=volume_price.daily_price,
monthly_price=volume_price.monthly_price,
)
serializer = InvoiceSerializer(new_invoice)
return Response(serializer.data)
# create not accepting tenant_id, delete it
del payload['tenant_id']
handler.create(payload)

View file

@ -1,22 +1,27 @@
from django.contrib import admin
from core.models import FlavorPrice, VolumePrice, FloatingIpsPrice, BillingProject, Invoice, InvoiceVolume, \
InvoiceFloatingIp, InvoiceInstance
InvoiceFloatingIp, InvoiceInstance, DynamicSetting
@admin.register(DynamicSetting)
class FlavorPriceAdmin(admin.ModelAdmin):
list_display = ('key', 'value', 'type')
@admin.register(FlavorPrice)
class FlavorPriceAdmin(admin.ModelAdmin):
list_display = ('flavor_id', 'daily_price', 'monthly_price')
list_display = ('flavor_id', 'hourly_price', 'monthly_price')
@admin.register(FloatingIpsPrice)
class FloatingIpsPriceAdmin(admin.ModelAdmin):
list_display = ('daily_price', 'monthly_price')
list_display = ('hourly_price', 'monthly_price')
@admin.register(VolumePrice)
class VolumePriceAdmin(admin.ModelAdmin):
list_display = ('volume_type_id', 'daily_price', 'monthly_price')
list_display = ('volume_type_id', 'hourly_price', 'monthly_price')
@admin.register(BillingProject)
@ -26,7 +31,7 @@ class BillingProjectAdmin(admin.ModelAdmin):
@admin.register(Invoice)
class InvoiceAdmin(admin.ModelAdmin):
list_display = ('__str__',)
list_display = ('__str__', 'project',)
@admin.register(InvoiceInstance)

View file

@ -0,0 +1,123 @@
import abc
from django.db import transaction
from django.utils import timezone
from core.models import Invoice, BillingProject
from core.component.base.invoice_handler import InvoiceHandler
class EventHandler(metaclass=abc.ABCMeta):
def __init__(self, invoice_handler):
self.invoice_handler: InvoiceHandler = invoice_handler
def get_tenant_progress_invoice(self, tenant_id):
"""
Get in progress invoice for specific tenant id.
Will create new instance if active invoice not found.
And will create new billing project if tenant id not found.
:param tenant_id: Tenant id to get the invoice from.
:return:
"""
invoice = Invoice.objects.filter(project__tenant_id=tenant_id, state=Invoice.InvoiceState.IN_PROGRESS).first()
if not invoice:
project = BillingProject.objects.get_or_create(tenant_id=tenant_id)
date_today = timezone.now()
month_first_day = date_today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
invoice = Invoice.objects.create(
project=project,
start_date=month_first_day,
state=Invoice.InvoiceState.IN_PROGRESS
)
return invoice
@abc.abstractmethod
def handle(self, event_type, raw_payload):
"""
Handle event from the message queue
:param event_type: The event type
:param raw_payload: Payload inside the message
:return:
"""
raise NotImplementedError()
@abc.abstractmethod
def clean_payload(self, event_type, raw_payload):
"""
Clean raw payload into payload that can be accepted by invoice handler
:param event_type: Current event type
:param raw_payload: Raw payload from messaging queue
:return:
"""
raise NotImplementedError()
@transaction.atomic
def handle_create(self, invoice: Invoice, event_type, raw_payload):
"""
Create new invoice component that will be saved to current invoice.
You need to call this method manually from handle() if you want to use it.
:param invoice: The invoice that will be saved into
:param event_type: Current event type
:param raw_payload: Raw payload from messaging queue.
:return:
"""
payload = self.clean_payload(event_type, raw_payload)
instance = self.invoice_handler.get_active_instance(invoice, payload)
if not instance:
payload['invoice'] = invoice
payload['start_date'] = timezone.now()
self.invoice_handler.create(payload)
return True
return False
@transaction.atomic
def handle_delete(self, invoice: Invoice, event_type, raw_payload):
"""
Close invoice component when delete event occurred.
You need to call this method manually from handle() if you want to use it.
:param invoice: The invoice that will be saved into
:param event_type: Current event type
:param raw_payload: Raw payload from messaging queue.
:return:
"""
payload = self.clean_payload(event_type, raw_payload)
instance = self.invoice_handler.get_active_instance(invoice, payload)
if instance:
self.invoice_handler.update_and_close(instance, payload)
return True
return False
@transaction.atomic
def handle_update(self, invoice: Invoice, event_type, raw_payload):
"""
Update invoice component when update event occurred.
You need to call this method manually from handle() if you want to use it.
:param invoice: The invoice that will be saved into
:param event_type: Current event type
:param raw_payload: Raw payload from messaging queue.
:return:
"""
payload = self.clean_payload(event_type, raw_payload)
instance = self.invoice_handler.get_active_instance(invoice, payload)
if instance:
if self.invoice_handler.is_price_dependency_changed(instance, payload):
self.invoice_handler.roll(instance, close_date=timezone.now(), update_payload=payload)
return True
if self.invoice_handler.is_informative_changed(instance, payload):
self.invoice_handler.update(instance, update_payload=payload)
return True
return False

View file

@ -0,0 +1,138 @@
import abc
from django.utils import timezone
from core.models import InvoiceComponentMixin, PriceMixin
class InvoiceHandler(metaclass=abc.ABCMeta):
INVOICE_CLASS = None
KEY_FIELD = None
INFORMATIVE_FIELDS = []
PRICE_DEPENDENCY_FIELDS = []
def create(self, payload):
"""
Create new invoice component
:param payload: the data that will be created
:return:
"""
price = self.get_price(payload)
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):
"""
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
:return:
"""
if update_payload is None:
update_payload = {}
if not instance.is_closed():
instance.close(close_date)
# Set primary ke to None, this will make save() to create a new row
instance.pk = None
instance.start_date = instance.end_date
instance.end_date = None
instance.created_at = None
instance.updated_at = None
# Update the new instance without saving
instance = self.update(instance, update_payload, save=False)
# Update the price
price = self.get_price(self.get_price_dependency_from_instance(instance))
instance.hourly_price = price.hourly_price
instance.monthly_price = price.monthly_price
instance.save()
return instance
def update(self, instance, update_payload, save=True):
"""
Update instance
:param instance: instance that will be updated
:param update_payload: new data
:param save: will it be saved or not
:return:
"""
for key, value in update_payload.items():
setattr(instance, key, value)
if save:
instance.save()
return instance
def update_and_close(self, instance, payload):
"""
:param instance: Instance that will be closed
:param payload: update payload
:return:
"""
self.update(instance, payload, save=False)
instance.close(timezone.now()) # Close will also save the instance
def is_informative_changed(self, instance, payload):
"""
Check whether informative field in instance is changed compared to the payload
:param instance: the instance that will be checked
:param payload: payload to compare
:return:
"""
for informative in self.INFORMATIVE_FIELDS:
if getattr(instance, informative) != payload[informative]:
return True
return False
def is_price_dependency_changed(self, instance, payload):
"""
Check whether price dependency field in instance is changed compared to the payload
:param instance: the instance that will be checked
:param payload: payload to compare
:return:
"""
for price_dependency in self.PRICE_DEPENDENCY_FIELDS:
if getattr(instance, price_dependency) != payload[price_dependency]:
return True
return False
def get_active_instance(self, invoice, payload):
"""
Get currently active invoice component instance.
Filtered by invoice and key field in payload
:param invoice: Invoice target
:param payload: Payload to get key field, please make sure there are value for the key field inside the payload
:return:
"""
kwargs = {"invoice": invoice, "end_date": None, self.KEY_FIELD: payload[self.KEY_FIELD]}
return self.INVOICE_CLASS.objects.filter(**kwargs).first()
def get_price_dependency_from_instance(self, instance):
"""
Get payload with price dependency field extracted from instance
:param instance: Instance to extract the price dependency
:return:
"""
return {field: getattr(instance, field) for field in self.PRICE_DEPENDENCY_FIELDS}
@abc.abstractmethod
def get_price(self, payload) -> PriceMixin:
"""
Get price based on payload
:param payload: the payload that will contain filter to get the price
:return:
"""
raise NotImplementedError()

View file

@ -0,0 +1,65 @@
from core.component.image.event_handler import ImageEventHandler
from core.component.image.invoice_handler import ImageInvoiceHandler
from core.component.router.event_handler import RouterEventHandler
from core.component.router.invoice_handler import RouterInvoiceHandler
from core.component.snapshot.event_handler import SnapshotEventHandler
from core.component.snapshot.invoice_handler import SnapshotInvoiceHandler
from core.models import InvoiceInstance, InvoiceVolume, InvoiceFloatingIp, FlavorPrice, FloatingIpsPrice, VolumePrice, \
RouterPrice, SnapshotPrice, InvoiceRouter, InvoiceSnapshot, ImagePrice, InvoiceImage
from core.component.floating_ips.event_handler import FloatingIpEventHandler
from core.component.floating_ips.invoice_handler import FloatingIpInvoiceHandler
from core.component.instances.event_handler import InstanceEventHandler
from core.component.instances.invoice_handler import InstanceInvoiceHandler
from core.component.labels import LABEL_INSTANCES, LABEL_VOLUMES, LABEL_FLOATING_IPS, LABEL_ROUTERS, LABEL_SNAPSHOTS, \
LABEL_IMAGES
from core.component.volume.event_handler import VolumeEventHandler
from core.component.volume.invoice_handler import VolumeInvoiceHandler
"""
Define a model that represent price for particular component
"""
PRICE_MODEL = {
"flavor": FlavorPrice,
"floating_ip": FloatingIpsPrice,
"volume": VolumePrice,
"router": RouterPrice,
"snapshot": SnapshotPrice,
"image": ImagePrice
}
"""
Define a model that represent a component in invoice.
The label that used for the key must be able to access through [Invoice] model
"""
INVOICE_COMPONENT_MODEL = {
LABEL_INSTANCES: InvoiceInstance,
LABEL_VOLUMES: InvoiceVolume,
LABEL_FLOATING_IPS: InvoiceFloatingIp,
LABEL_ROUTERS: InvoiceRouter,
LABEL_SNAPSHOTS: InvoiceSnapshot,
LABEL_IMAGES: InvoiceImage
}
"""
Define a class that handle the event from message queue
"""
EVENT_HANDLER = {
LABEL_INSTANCES: InstanceEventHandler,
LABEL_VOLUMES: VolumeEventHandler,
LABEL_FLOATING_IPS: FloatingIpEventHandler,
LABEL_ROUTERS: RouterEventHandler,
LABEL_SNAPSHOTS: SnapshotEventHandler,
LABEL_IMAGES: ImageEventHandler
}
"""
Define an instance that handle invoice creation or update
"""
INVOICE_HANDLER = {
LABEL_INSTANCES: InstanceInvoiceHandler(),
LABEL_VOLUMES: VolumeInvoiceHandler(),
LABEL_FLOATING_IPS: FloatingIpInvoiceHandler(),
LABEL_ROUTERS: RouterInvoiceHandler(),
LABEL_SNAPSHOTS: SnapshotInvoiceHandler(),
LABEL_IMAGES: ImageInvoiceHandler()
}

View file

@ -0,0 +1,23 @@
from core.component.base.event_handler import EventHandler
from core.component.floating_ips.invoice_handler import FloatingIpInvoiceHandler
class FloatingIpEventHandler(EventHandler):
def handle(self, event_type, raw_payload):
if event_type == 'floatingip.create.end':
tenant_id = raw_payload['floatingip']['tenant_id']
invoice = self.get_tenant_progress_invoice(tenant_id)
self.handle_create(invoice, event_type, raw_payload)
if event_type == 'floatingip.delete.end':
tenant_id = raw_payload['floatingip']['tenant_id']
invoice = self.get_tenant_progress_invoice(tenant_id)
self.handle_delete(invoice, event_type, raw_payload)
def clean_payload(self, event_type, raw_payload):
payload = {
"fip_id": raw_payload['floatingip']['id'],
"ip": raw_payload['floatingip']['floating_ip_address'],
}
return payload

View file

@ -0,0 +1,12 @@
from core.models import FloatingIpsPrice, InvoiceFloatingIp, PriceMixin
from core.component.base.invoice_handler import InvoiceHandler
class FloatingIpInvoiceHandler(InvoiceHandler):
INVOICE_CLASS = InvoiceFloatingIp
KEY_FIELD = "fip_id"
PRICE_DEPENDENCY_FIELDS = []
INFORMATIVE_FIELDS = ["ip"]
def get_price(self, payload) -> PriceMixin:
return FloatingIpsPrice.objects.first()

View file

@ -0,0 +1,28 @@
from core.component.base.event_handler import EventHandler
class ImageEventHandler(EventHandler):
def handle(self, event_type, raw_payload):
if event_type == 'image.activate':
tenant_id = raw_payload['owner']
invoice = self.get_tenant_progress_invoice(tenant_id)
self.handle_create(invoice, event_type, raw_payload)
if event_type == 'image.delete':
tenant_id = raw_payload['owner']
invoice = self.get_tenant_progress_invoice(tenant_id)
self.handle_delete(invoice, event_type, raw_payload)
if event_type == 'image.update':
tenant_id = raw_payload['owner']
invoice = self.get_tenant_progress_invoice(tenant_id)
self.handle_update(invoice, event_type, raw_payload)
def clean_payload(self, event_type, raw_payload):
payload = {
"image_id": raw_payload['id'],
"space_allocation_gb": raw_payload['size'] / 1024 / 1024 / 1024,
"name": raw_payload['name'] or raw_payload['id'],
}
return payload

View file

@ -0,0 +1,12 @@
from core.component.base.invoice_handler import InvoiceHandler
from core.models import PriceMixin, InvoiceImage, ImagePrice
class ImageInvoiceHandler(InvoiceHandler):
INVOICE_CLASS = InvoiceImage
KEY_FIELD = "image_id"
PRICE_DEPENDENCY_FIELDS = ["space_allocation_gb"]
INFORMATIVE_FIELDS = ["name"]
def get_price(self, payload) -> PriceMixin:
return ImagePrice.objects.first()

View file

@ -0,0 +1,25 @@
from core.component.base.event_handler import EventHandler
class InstanceEventHandler(EventHandler):
def handle(self, event_type, raw_payload):
if event_type == 'compute.instance.update':
tenant_id = raw_payload['tenant_id']
invoice = self.get_tenant_progress_invoice(tenant_id)
is_updated = self.handle_update(invoice, event_type, raw_payload)
if not is_updated and raw_payload['state'] == 'active':
self.handle_create(invoice, event_type, raw_payload)
if not is_updated and raw_payload['state'] == 'deleted':
self.handle_delete(invoice, event_type, raw_payload)
def clean_payload(self, event_type, raw_payload):
payload = {
"instance_id": raw_payload['instance_id'],
"flavor_id": raw_payload['instance_flavor_id'],
"name": raw_payload['display_name'],
}
return payload

View file

@ -0,0 +1,12 @@
from core.models import FlavorPrice, InvoiceInstance, PriceMixin
from core.component.base.invoice_handler import InvoiceHandler
class InstanceInvoiceHandler(InvoiceHandler):
INVOICE_CLASS = InvoiceInstance
KEY_FIELD = "instance_id"
PRICE_DEPENDENCY_FIELDS = ['flavor_id']
INFORMATIVE_FIELDS = ['name']
def get_price(self, payload) -> PriceMixin:
return FlavorPrice.objects.filter(flavor_id=payload['flavor_id']).first()

9
core/component/labels.py Normal file
View file

@ -0,0 +1,9 @@
LABEL_INSTANCES = "instances"
LABEL_VOLUMES = "volumes"
LABEL_FLOATING_IPS = "floating_ips"
LABEL_ROUTERS = "routers"
LABEL_SNAPSHOTS = "snapshots"
LABEL_IMAGES = "images"
INVOICE_COMPONENT_LABELS = [LABEL_INSTANCES, LABEL_VOLUMES, LABEL_FLOATING_IPS, LABEL_ROUTERS, LABEL_SNAPSHOTS,
LABEL_IMAGES]

View file

@ -0,0 +1,43 @@
from core.component.base.event_handler import EventHandler
from core.component.floating_ips.invoice_handler import FloatingIpInvoiceHandler
class RouterEventHandler(EventHandler):
def is_external_gateway_set(self, raw_payload):
return raw_payload['router']['external_gateway_info'] is not None
def handle(self, event_type, raw_payload):
# Case: Creating router with external gateway
if event_type == 'router.create.end' and self.is_external_gateway_set(raw_payload):
tenant_id = raw_payload['router']['tenant_id']
invoice = self.get_tenant_progress_invoice(tenant_id)
self.handle_create(invoice, event_type, raw_payload)
if event_type == 'router.update.end':
tenant_id = raw_payload['router']['tenant_id']
invoice = self.get_tenant_progress_invoice(tenant_id)
# Handel update for existing instance
self.handle_update(invoice, event_type, raw_payload)
# Case: Existing router set gateway
if self.is_external_gateway_set(raw_payload):
self.handle_create(invoice, event_type, raw_payload)
# Case: Existing router remove gateway
if not self.is_external_gateway_set(raw_payload):
self.handle_delete(invoice, event_type, raw_payload)
# Case: Delete router
if event_type == 'router.delete.end':
tenant_id = raw_payload['router']['tenant_id']
invoice = self.get_tenant_progress_invoice(tenant_id)
self.handle_delete(invoice, event_type, raw_payload)
def clean_payload(self, event_type, raw_payload):
payload = {
"router_id": raw_payload['router']['id'],
"name": raw_payload['router']['name'],
}
return payload

View file

@ -0,0 +1,12 @@
from core.component.base.invoice_handler import InvoiceHandler
from core.models import PriceMixin, InvoiceRouter, RouterPrice
class RouterInvoiceHandler(InvoiceHandler):
INVOICE_CLASS = InvoiceRouter
KEY_FIELD = "router_id"
PRICE_DEPENDENCY_FIELDS = []
INFORMATIVE_FIELDS = ["name"]
def get_price(self, payload) -> PriceMixin:
return RouterPrice.objects.first()

View file

@ -0,0 +1,29 @@
from core.component.base.event_handler import EventHandler
from core.component.floating_ips.invoice_handler import FloatingIpInvoiceHandler
class SnapshotEventHandler(EventHandler):
def handle(self, event_type, raw_payload):
if event_type == 'snapshot.create.end':
tenant_id = raw_payload['tenant_id']
invoice = self.get_tenant_progress_invoice(tenant_id)
self.handle_create(invoice, event_type, raw_payload)
if event_type == 'snapshot.delete.end':
tenant_id = raw_payload['tenant_id']
invoice = self.get_tenant_progress_invoice(tenant_id)
self.handle_delete(invoice, event_type, raw_payload)
if event_type == 'snapshot.update.end':
tenant_id = raw_payload['tenant_id']
invoice = self.get_tenant_progress_invoice(tenant_id)
self.handle_update(invoice, event_type, raw_payload)
def clean_payload(self, event_type, raw_payload):
payload = {
"snapshot_id": raw_payload['snapshot_id'],
"space_allocation_gb": raw_payload['volume_size'],
"name": raw_payload['display_name'],
}
return payload

View file

@ -0,0 +1,12 @@
from core.component.base.invoice_handler import InvoiceHandler
from core.models import PriceMixin, InvoiceSnapshot, SnapshotPrice
class SnapshotInvoiceHandler(InvoiceHandler):
INVOICE_CLASS = InvoiceSnapshot
KEY_FIELD = "snapshot_id"
PRICE_DEPENDENCY_FIELDS = ["space_allocation_gb"]
INFORMATIVE_FIELDS = ["name"]
def get_price(self, payload) -> PriceMixin:
return SnapshotPrice.objects.first()

View file

@ -0,0 +1,29 @@
from core.component.base.event_handler import EventHandler
class VolumeEventHandler(EventHandler):
def handle(self, event_type, raw_payload):
if event_type == 'volume.create.end':
tenant_id = raw_payload['tenant_id']
invoice = self.get_tenant_progress_invoice(tenant_id)
self.handle_create(invoice, event_type, raw_payload)
if event_type == 'volume.delete.end':
tenant_id = raw_payload['tenant_id']
invoice = self.get_tenant_progress_invoice(tenant_id)
self.handle_delete(invoice, event_type, raw_payload)
if event_type in ['volume.resize.end', 'volume.update.end', 'volume.retype']:
tenant_id = raw_payload['tenant_id']
invoice = self.get_tenant_progress_invoice(tenant_id)
self.handle_update(invoice, event_type, raw_payload)
def clean_payload(self, event_type, raw_payload):
payload = {
"volume_id": raw_payload['volume_id'],
"volume_type_id": raw_payload['volume_type'],
"volume_name": raw_payload['display_name'],
"space_allocation_gb": raw_payload['size'],
}
return payload

View file

@ -0,0 +1,12 @@
from core.models import VolumePrice, InvoiceVolume, PriceMixin
from core.component.base.invoice_handler import InvoiceHandler
class VolumeInvoiceHandler(InvoiceHandler):
INVOICE_CLASS = InvoiceVolume
KEY_FIELD = "volume_id"
PRICE_DEPENDENCY_FIELDS = ['volume_type_id', 'space_allocation_gb']
INFORMATIVE_FIELDS = ['volume_name']
def get_price(self, payload) -> PriceMixin:
return VolumePrice.objects.filter(volume_type_id=payload['volume_type_id']).first()

29
core/event_endpoint.py Normal file
View file

@ -0,0 +1,29 @@
import logging
from oslo_messaging import NotificationResult
from core.component import component
from core.utils.dynamic_setting import get_dynamic_settings, BILLING_ENABLED, get_dynamic_setting
LOG = logging.getLogger("rintik_notification")
class EventEndpoint(object):
def __init__(self):
self.event_handler = [
cls(component.INVOICE_HANDLER[label]) for label, cls in component.EVENT_HANDLER.items()
]
def info(self, ctxt, publisher_id, event_type, payload, metadata):
LOG.info("=== Event Received ===")
LOG.info("Event Type: " + str(event_type))
LOG.info("Payload: " + str(payload))
if not get_dynamic_setting(BILLING_ENABLED):
return NotificationResult.HANDLED
# TODO: Error Handling
for handler in self.event_handler:
handler.handle(event_type, payload)
return NotificationResult.HANDLED

View file

@ -10,7 +10,7 @@ from oslo_messaging import notify # noqa
from django.core.management.base import BaseCommand
from core.notification_endpoint import NotifyEndpoint
from core.event_endpoint import EventEndpoint
LOG = logging.getLogger("rintik_notification")
@ -42,7 +42,7 @@ class Command(BaseCommand):
transport.cleanup()
def notify_server(self, transport, topics):
endpoints = [NotifyEndpoint()]
endpoints = [EventEndpoint()]
targets = list(map(lambda t: messaging.Target(topic=t), topics))
server = notify.get_notification_listener(
transport,

View file

@ -1,97 +0,0 @@
import logging
from typing import List
from django.core.management import BaseCommand
from django.utils import timezone
from core.models import Invoice, InvoiceInstance, InvoiceFloatingIp, InvoiceVolume
LOG = logging.getLogger("rintik_new_invoice")
class Command(BaseCommand):
help = 'Rintik New Invoice'
def handle(self, *args, **options):
self.close_date = timezone.now()
self.tax_pertentage = 0
active_invoices = Invoice.objects.filter(state=Invoice.InvoiceState.IN_PROGRESS).all()
for active_invoice in active_invoices:
self.close_active_invoice(active_invoice)
def close_active_invoice(self, active_invoice: Invoice):
# Close Active instances
active_instances: List[InvoiceInstance] = active_invoice.instances.filter(end_date=None).all()
for instance in active_instances:
instance.end_date = self.close_date
instance.save()
# Close Active Floating
active_ips: List[InvoiceFloatingIp] = active_invoice.floating_ips.filter(end_date=None).all()
for ip in active_ips:
ip.end_date = self.close_date
ip.save()
# Close Active Volume
active_volumes: List[InvoiceVolume] = active_invoice.volumes.filter(end_date=None).all()
for volume in active_volumes:
volume.end_date = self.close_date
volume.save()
# Save current instance
active_invoice.state = Invoice.InvoiceState.FINISHED
active_invoice.end_date = self.close_date
active_invoice.tax = self.tax_pertentage * active_invoice.subtotal
active_invoice.total = active_invoice.tax + active_invoice.subtotal
active_invoice.save()
# Creating new Invoice
new_invoice = Invoice.objects.create(
project=active_invoice.project,
start_date=self.close_date,
state=Invoice.InvoiceState.IN_PROGRESS
)
new_invoice.save()
# Cloning Active Instance to Continue in next invoice
# Using the same price
for instance in active_instances:
InvoiceInstance.objects.create(
invoice=new_invoice,
instance_id=instance.instance_id,
name=instance.name,
flavor_id=instance.flavor_id,
current_state=instance.current_state,
start_date=instance.end_date,
daily_price=instance.daily_price,
monthly_price=instance.monthly_price,
)
# Cloning Active Ips to Continue in next invoice
# Using the same price
for ip in active_ips:
InvoiceFloatingIp.objects.create(
invoice=new_invoice,
fip_id=ip.fip_id,
ip=ip.ip,
current_state=ip.current_state,
start_date=ip.end_date,
daily_price=ip.daily_price,
monthly_price=ip.monthly_price,
)
# Cloning Active Volumes to Continue in next invoice
# Using the same price
for volume in active_volumes:
InvoiceVolume.objects.create(
invoice=new_invoice,
volume_id=volume.volume_id,
volume_name=volume.volume_name,
volume_type_id=volume.volume_type_id,
space_allocation_gb=volume.space_allocation_gb,
current_state=volume.current_state,
start_date=volume.end_date,
daily_price=volume.daily_price,
monthly_price=volume.monthly_price,
)

View file

@ -0,0 +1,55 @@
import logging
from typing import Mapping, Dict, Iterable
from django.core.management import BaseCommand
from django.utils import timezone
from core.models import Invoice, InvoiceComponentMixin
from core.component import component, labels
from core.utils.dynamic_setting import get_dynamic_setting, BILLING_ENABLED, INVOICE_TAX
LOG = logging.getLogger("rintik_new_invoice")
class Command(BaseCommand):
help = 'Rintik New Invoice'
def handle(self, *args, **options):
if not get_dynamic_setting(BILLING_ENABLED):
return
self.close_date = timezone.now()
self.tax_pertentage = get_dynamic_setting(INVOICE_TAX)
active_invoices = Invoice.objects.filter(state=Invoice.InvoiceState.IN_PROGRESS).all()
for active_invoice in active_invoices:
self.close_active_invoice(active_invoice)
def close_active_invoice(self, active_invoice: Invoice):
active_components_map: Dict[str, Iterable[InvoiceComponentMixin]] = {}
for label in labels.INVOICE_COMPONENT_LABELS:
active_components_map[label] = getattr(active_invoice, label).filter(end_date=None).all()
# Close Invoice Component
for active_component in active_components_map[label]:
active_component.close(self.close_date)
# Finish current invoice
active_invoice.close(self.close_date, self.tax_pertentage)
# Creating new Invoice
new_invoice = Invoice.objects.create(
project=active_invoice.project,
start_date=self.close_date,
state=Invoice.InvoiceState.IN_PROGRESS
)
new_invoice.save()
# Cloning active component to continue in next invoice
for label, active_components in active_components_map.items():
handler = component.INVOICE_HANDLER[label]
for active_component in active_components:
handler.roll(active_component, self.close_date, update_payload={
"invoice": new_invoice
})

View file

@ -1,4 +1,4 @@
# Generated by Django 3.2.6 on 2021-08-28 07:05
# Generated by Django 3.2.6 on 2021-10-10 13:08
from django.db import migrations, models
import django.db.models.deletion
@ -17,112 +17,141 @@ class Migration(migrations.Migration):
name='BillingProject',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('project_id', models.CharField(max_length=256)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('tenant_id', models.CharField(max_length=256)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='FlavorPrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('flavor_id', models.CharField(max_length=256)),
('daily_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('daily_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
('flavor_id', models.CharField(max_length=256)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='FloatingIpsPrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('daily_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('daily_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Invoice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_created=True)),
('invoice_number', models.CharField(default=None, max_length=256, null=True)),
('start_date', models.DateTimeField()),
('end_date', models.DateTimeField()),
('due_date', models.DateTimeField(default=None, null=True)),
('state', models.IntegerField(choices=[(1, 'In Progress'), (2, 'Waiting Payment'), (100, 'Finished')])),
('subtotal', models.BigIntegerField(default=None, null=True)),
('tax', models.BigIntegerField(default=None, null=True)),
('total', models.BigIntegerField(default=None, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('start_date', models.DateTimeField()),
('end_date', models.DateTimeField(blank=True, default=None, null=True)),
('state', models.IntegerField(choices=[(1, 'In Progress'), (100, 'Finished')])),
('tax_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('tax', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
('total_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('total', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.billingproject')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='VolumePrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('volume_type_id', models.CharField(max_length=256)),
('daily_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('daily_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
('volume_type_id', models.CharField(max_length=256)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='InvoiceVolume',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_created=True)),
('volume_id', models.CharField(max_length=266)),
('volume_type_id', models.CharField(max_length=266)),
('space_allocation_gb', models.IntegerField()),
('current_state', models.CharField(max_length=256)),
('start_date', models.DateTimeField()),
('end_date', models.DateTimeField()),
('daily_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('daily_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.invoice')),
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
('start_date', models.DateTimeField()),
('end_date', models.DateTimeField(blank=True, default=None, null=True)),
('volume_id', models.CharField(max_length=256)),
('volume_type_id', models.CharField(max_length=256)),
('space_allocation_gb', models.IntegerField()),
('volume_name', models.CharField(max_length=256)),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volumes', to='core.invoice')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='InvoiceInstance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_created=True)),
('instance_id', models.CharField(max_length=266)),
('name', models.CharField(max_length=256)),
('flavor_id', models.CharField(max_length=256)),
('current_state', models.CharField(max_length=256)),
('start_date', models.DateTimeField()),
('end_date', models.DateTimeField()),
('daily_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('daily_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.invoice')),
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
('start_date', models.DateTimeField()),
('end_date', models.DateTimeField(blank=True, default=None, null=True)),
('instance_id', models.CharField(max_length=266)),
('flavor_id', models.CharField(max_length=256)),
('name', models.CharField(max_length=256)),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='core.invoice')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='InvoiceFloatingIp',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_created=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
('start_date', models.DateTimeField()),
('end_date', models.DateTimeField(blank=True, default=None, null=True)),
('fip_id', models.CharField(max_length=266)),
('ip', models.CharField(max_length=256)),
('current_state', models.CharField(max_length=256)),
('start_date', models.DateTimeField()),
('end_date', models.DateTimeField()),
('daily_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('daily_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
('updated_at', models.DateTimeField(auto_now=True)),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.invoice')),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='floating_ips', to='core.invoice')),
],
options={
'abstract': False,
},
),
]

View file

@ -1,58 +0,0 @@
# Generated by Django 3.2.6 on 2021-08-28 11:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='invoice',
name='created_at',
field=models.DateTimeField(auto_created=True, blank=True, null=True),
),
migrations.AlterField(
model_name='invoice',
name='due_date',
field=models.DateTimeField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='invoice',
name='end_date',
field=models.DateTimeField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='invoice',
name='tax',
field=models.BigIntegerField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='invoice',
name='total',
field=models.BigIntegerField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='invoice',
name='updated_at',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AlterField(
model_name='invoiceinstance',
name='created_at',
field=models.DateTimeField(auto_created=True, blank=True, null=True),
),
migrations.AlterField(
model_name='invoiceinstance',
name='end_date',
field=models.DateTimeField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='invoiceinstance',
name='updated_at',
field=models.DateTimeField(auto_now=True, null=True),
),
]

View file

@ -0,0 +1,25 @@
# Generated by Django 3.2.6 on 2021-10-13 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='DynamicSetting',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(db_index=True, max_length=256, unique=True)),
('value', models.TextField()),
('type', models.IntegerField(choices=[(1, 'Boolean'), (2, 'Int'), (3, 'Str'), (4, 'Json')])),
],
options={
'abstract': False,
},
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2021-10-13 17:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_dynamicsetting'),
]
operations = [
migrations.AlterField(
model_name='invoicevolume',
name='space_allocation_gb',
field=models.FloatField(),
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 3.2.6 on 2021-08-28 11:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_auto_20210828_1112'),
]
operations = [
migrations.AlterField(
model_name='invoice',
name='invoice_number',
field=models.CharField(blank=True, default=None, max_length=256, null=True),
),
migrations.AlterField(
model_name='invoice',
name='subtotal',
field=models.BigIntegerField(blank=True, default=None, null=True),
),
]

View file

@ -1,22 +0,0 @@
# Generated by Django 3.2.6 on 2021-08-28 11:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0003_auto_20210828_1115'),
]
operations = [
migrations.RenameField(
model_name='billingproject',
old_name='project_id',
new_name='tenant_id',
),
migrations.RemoveField(
model_name='invoice',
name='invoice_number',
),
]

View file

@ -0,0 +1,86 @@
# Generated by Django 3.2.6 on 2021-10-14 05:47
from django.db import migrations, models
import django.db.models.deletion
import djmoney.models.fields
class Migration(migrations.Migration):
dependencies = [
('core', '0003_alter_invoicevolume_space_allocation_gb'),
]
operations = [
migrations.CreateModel(
name='RouterPrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='SnapshotPrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='InvoiceSnapshot',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
('start_date', models.DateTimeField()),
('end_date', models.DateTimeField(blank=True, default=None, null=True)),
('snapshot_id', models.CharField(max_length=256)),
('space_allocation_gb', models.FloatField()),
('name', models.CharField(max_length=256)),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='snapshots', to='core.invoice')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='InvoiceRouter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
('start_date', models.DateTimeField()),
('end_date', models.DateTimeField(blank=True, default=None, null=True)),
('router_id', models.CharField(max_length=256)),
('name', models.CharField(max_length=256)),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='routers', to='core.invoice')),
],
options={
'abstract': False,
},
),
]

View file

@ -1,33 +0,0 @@
# Generated by Django 3.2.6 on 2021-08-30 09:31
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0004_auto_20210828_1137'),
]
operations = [
migrations.RemoveField(
model_name='invoice',
name='due_date',
),
migrations.AlterField(
model_name='invoicefloatingip',
name='invoice',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='floating_ips', to='core.invoice'),
),
migrations.AlterField(
model_name='invoiceinstance',
name='invoice',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='core.invoice'),
),
migrations.AlterField(
model_name='invoicevolume',
name='invoice',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volumes', to='core.invoice'),
),
]

View file

@ -0,0 +1,51 @@
# Generated by Django 3.2.6 on 2021-10-29 04:22
from django.db import migrations, models
import django.db.models.deletion
import djmoney.models.fields
class Migration(migrations.Migration):
dependencies = [
('core', '0004_invoicerouter_invoicesnapshot_routerprice_snapshotprice'),
]
operations = [
migrations.CreateModel(
name='ImagePrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='InvoiceImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
('start_date', models.DateTimeField()),
('end_date', models.DateTimeField(blank=True, default=None, null=True)),
('image_id', models.CharField(max_length=256)),
('space_allocation_gb', models.FloatField()),
('name', models.CharField(max_length=256)),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='core.invoice')),
],
options={
'abstract': False,
},
),
]

View file

@ -1,103 +0,0 @@
# Generated by Django 3.2.6 on 2021-08-31 04:00
from django.db import migrations, models
import djmoney.models.fields
class Migration(migrations.Migration):
dependencies = [
('core', '0005_auto_20210830_0931'),
]
operations = [
migrations.RemoveField(
model_name='invoice',
name='subtotal',
),
migrations.AddField(
model_name='invoice',
name='tax_currency',
field=djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3),
),
migrations.AddField(
model_name='invoice',
name='total_currency',
field=djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3),
),
migrations.AlterField(
model_name='flavorprice',
name='daily_price',
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
),
migrations.AlterField(
model_name='flavorprice',
name='monthly_price',
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
),
migrations.AlterField(
model_name='floatingipsprice',
name='daily_price',
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
),
migrations.AlterField(
model_name='floatingipsprice',
name='monthly_price',
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
),
migrations.AlterField(
model_name='invoice',
name='state',
field=models.IntegerField(choices=[(1, 'In Progress'), (100, 'Finished')]),
),
migrations.AlterField(
model_name='invoice',
name='tax',
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True),
),
migrations.AlterField(
model_name='invoice',
name='total',
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True),
),
migrations.AlterField(
model_name='invoicefloatingip',
name='daily_price',
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
),
migrations.AlterField(
model_name='invoicefloatingip',
name='monthly_price',
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
),
migrations.AlterField(
model_name='invoiceinstance',
name='daily_price',
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
),
migrations.AlterField(
model_name='invoiceinstance',
name='monthly_price',
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
),
migrations.AlterField(
model_name='invoicevolume',
name='daily_price',
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
),
migrations.AlterField(
model_name='invoicevolume',
name='monthly_price',
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
),
migrations.AlterField(
model_name='volumeprice',
name='daily_price',
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
),
migrations.AlterField(
model_name='volumeprice',
name='monthly_price',
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 3.2.6 on 2021-08-31 04:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0006_auto_20210831_0400'),
]
operations = [
migrations.AlterField(
model_name='invoice',
name='created_at',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='invoiceinstance',
name='created_at',
field=models.DateTimeField(auto_now_add=True, null=True),
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 3.2.6 on 2021-09-01 06:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0007_auto_20210831_0403'),
]
operations = [
migrations.AlterField(
model_name='invoicefloatingip',
name='created_at',
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name='invoicevolume',
name='created_at',
field=models.DateTimeField(auto_now_add=True),
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 3.2.6 on 2021-09-01 06:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0008_auto_20210901_0636'),
]
operations = [
migrations.AlterField(
model_name='invoicefloatingip',
name='end_date',
field=models.DateTimeField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='invoicevolume',
name='end_date',
field=models.DateTimeField(blank=True, default=None, null=True),
),
]

View file

@ -1,29 +0,0 @@
# Generated by Django 3.2.6 on 2021-09-06 13:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0009_auto_20210901_0644'),
]
operations = [
migrations.AddField(
model_name='invoicevolume',
name='volume_name',
field=models.CharField(default='', max_length=256),
preserve_default=False,
),
migrations.AlterField(
model_name='invoicevolume',
name='volume_id',
field=models.CharField(max_length=256),
),
migrations.AlterField(
model_name='invoicevolume',
name='volume_type_id',
field=models.CharField(max_length=256),
),
]

View file

@ -1,34 +1,66 @@
from datetime import timedelta
import math
from django.conf import settings
from django.db import models
from django.utils import timezone
from djmoney.models.fields import MoneyField
from djmoney.money import Money
from core.component import labels
from core.utils.model_utils import BaseModel, TimestampMixin, PriceMixin, InvoiceComponentMixin
class FlavorPrice(models.Model):
# region Dynamic Setting
class DynamicSetting(BaseModel):
class DataType(models.IntegerChoices):
BOOLEAN = 1
INT = 2
STR = 3
JSON = 4
key = models.CharField(max_length=256, unique=True, db_index=True)
value = models.TextField()
type = models.IntegerField(choices=DataType.choices)
# end region
# region Pricing
class FlavorPrice(BaseModel, TimestampMixin, PriceMixin):
flavor_id = models.CharField(max_length=256)
daily_price = MoneyField(max_digits=10, decimal_places=0)
monthly_price = MoneyField(max_digits=10, decimal_places=0)
class FloatingIpsPrice(models.Model):
daily_price = MoneyField(max_digits=10, decimal_places=0)
monthly_price = MoneyField(max_digits=10, decimal_places=0)
class FloatingIpsPrice(BaseModel, TimestampMixin, PriceMixin):
# No need for any additional field
pass
class VolumePrice(models.Model):
class VolumePrice(BaseModel, TimestampMixin, PriceMixin):
volume_type_id = models.CharField(max_length=256)
daily_price = MoneyField(max_digits=10, decimal_places=0)
monthly_price = MoneyField(max_digits=10, decimal_places=0)
class BillingProject(models.Model):
class RouterPrice(BaseModel, TimestampMixin, PriceMixin):
# No need for any additional field
pass
class SnapshotPrice(BaseModel, TimestampMixin, PriceMixin):
# No need for any additional field
pass
class ImagePrice(BaseModel, TimestampMixin, PriceMixin):
# No need for any additional field
pass
# end region
# region Invoicing
class BillingProject(BaseModel, TimestampMixin):
tenant_id = models.CharField(max_length=256)
class Invoice(models.Model):
class Invoice(BaseModel, TimestampMixin):
class InvoiceState(models.IntegerChoices):
IN_PROGRESS = 1
FINISHED = 100
@ -39,122 +71,111 @@ class Invoice(models.Model):
state = models.IntegerField(choices=InvoiceState.choices)
tax = MoneyField(max_digits=10, decimal_places=0, default=None, blank=True, null=True)
total = MoneyField(max_digits=10, decimal_places=0, default=None, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)
updated_at = models.DateTimeField(auto_now=True, blank=True, null=True)
@property
def subtotal(self):
# Need to optimize? currently price is calculated on the fly, maybe need to save to db to make performance faster?
instance_price = sum(map(lambda x: x.price_charged, self.instances.all()))
fip_price = sum(map(lambda x: x.price_charged, self.floating_ips.all()))
volume_price = sum(map(lambda x: x.price_charged, self.volumes.all()))
price = instance_price + fip_price + volume_price
# Need to optimize?
# currently price is calculated on the fly, maybe need to save to db to make performance faster?
# or using cache?
price = 0
for component_relation_label in labels.INVOICE_COMPONENT_LABELS:
relation_all_row = getattr(self, component_relation_label).all()
price += sum(map(lambda x: x.price_charged, relation_all_row))
if price == 0:
return Money(amount=price, currency=settings.DEFAULT_CURRENCY)
return instance_price + fip_price + volume_price
return price
def close(self, date, tax_percentage):
self.state = Invoice.InvoiceState.FINISHED
self.end_date = date
self.tax = tax_percentage * self.subtotal
self.total = self.tax + self.subtotal
self.save()
class InvoiceInstance(models.Model):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name='instances')
# end region
# region Invoice Component
class InvoiceInstance(BaseModel, InvoiceComponentMixin):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name=labels.LABEL_INSTANCES)
# Key
instance_id = models.CharField(max_length=266)
name = models.CharField(max_length=256)
# Price Dependency
flavor_id = models.CharField(max_length=256)
current_state = models.CharField(max_length=256)
start_date = models.DateTimeField()
end_date = models.DateTimeField(default=None, blank=True, null=True)
daily_price = MoneyField(max_digits=10, decimal_places=0)
monthly_price = MoneyField(max_digits=10, decimal_places=0)
created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)
updated_at = models.DateTimeField(auto_now=True, blank=True, null=True)
@property
def adjusted_end_date(self):
current_date = timezone.now()
if self.end_date:
end_date = self.end_date
else:
end_date = current_date
return end_date
@property
def price_charged(self):
# 1. Belum 1 hari -> Kehitung 1 hari
# 2. Perhitungan bulanan
# 3. 1 Bulan 15 hari gimana?
# TODO: Fix price calculation
# Currently only calculate daily price and it can return zero if end date not yet 1 day
end_date = self.adjusted_end_date
end_date += timedelta(days=1)
days = (end_date - self.start_date).days
return self.daily_price * days
# Informative
name = models.CharField(max_length=256)
class InvoiceFloatingIp(models.Model):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name='floating_ips')
class InvoiceFloatingIp(BaseModel, InvoiceComponentMixin):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name=labels.LABEL_FLOATING_IPS)
# Key
fip_id = models.CharField(max_length=266)
# Informative
ip = models.CharField(max_length=256)
current_state = models.CharField(max_length=256)
start_date = models.DateTimeField()
end_date = models.DateTimeField(default=None, blank=True, null=True)
daily_price = MoneyField(max_digits=10, decimal_places=0)
monthly_price = MoneyField(max_digits=10, decimal_places=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@property
def adjusted_end_date(self):
current_date = timezone.now()
if self.end_date:
end_date = self.end_date
else:
end_date = current_date
return end_date
@property
def price_charged(self):
# TODO: Fix price calculation
# Currently only calculate daily price and it can return zero if end date not yet 1 day
end_date = self.adjusted_end_date
end_date += timedelta(days=1)
days = (end_date - self.start_date).days
return self.daily_price * days
class InvoiceVolume(models.Model):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name='volumes')
class InvoiceVolume(BaseModel, InvoiceComponentMixin):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name=labels.LABEL_VOLUMES)
# Key
volume_id = models.CharField(max_length=256)
volume_name = models.CharField(max_length=256)
# Price Dependency
volume_type_id = models.CharField(max_length=256)
space_allocation_gb = models.IntegerField()
current_state = models.CharField(max_length=256)
start_date = models.DateTimeField()
end_date = models.DateTimeField(default=None, blank=True, null=True)
daily_price = MoneyField(max_digits=10, decimal_places=0)
monthly_price = MoneyField(max_digits=10, decimal_places=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
space_allocation_gb = models.FloatField()
@property
def adjusted_end_date(self):
current_date = timezone.now()
if self.end_date:
end_date = self.end_date
else:
end_date = current_date
return end_date
# Informative
volume_name = models.CharField(max_length=256)
@property
def price_charged(self):
# TODO: Fix price calculation
# Currently only calculate daily price and it can return zero if end date not yet 1 day
end_date = self.adjusted_end_date
end_date += timedelta(days=1)
price_without_allocation = super().price_charged
return price_without_allocation * math.ceil(self.space_allocation_gb)
days = (end_date - self.start_date).days
return self.daily_price * self.space_allocation_gb * days
class InvoiceRouter(BaseModel, InvoiceComponentMixin):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name=labels.LABEL_ROUTERS)
# Key
router_id = models.CharField(max_length=256)
# Informative
name = models.CharField(max_length=256)
class InvoiceSnapshot(BaseModel, InvoiceComponentMixin):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name=labels.LABEL_SNAPSHOTS)
# Key
snapshot_id = models.CharField(max_length=256)
# Price Dependency
space_allocation_gb = models.FloatField()
# Informative
name = models.CharField(max_length=256)
@property
def price_charged(self):
price_without_allocation = super().price_charged
return price_without_allocation * math.ceil(self.space_allocation_gb)
class InvoiceImage(BaseModel, InvoiceComponentMixin):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name=labels.LABEL_IMAGES)
# Key
image_id = models.CharField(max_length=256)
# Price Dependency
space_allocation_gb = models.FloatField()
# Informative
name = models.CharField(max_length=256)
#
# @property
# def price_charged(self):
# price_without_allocation = super().price_charged
# return price_without_allocation * math.ceil(self.space_allocation_gb)
# end region

View file

@ -1,28 +0,0 @@
import logging
from oslo_messaging import NotificationResult
from core.notification_handler.compute_handler import ComputeHandler
from core.notification_handler.network_handler import NetworkHandler
from core.notification_handler.volume_handler import VolumeHandler
LOG = logging.getLogger("rintik_notification")
class NotifyEndpoint(object):
handlers = [
ComputeHandler(),
NetworkHandler(),
VolumeHandler()
]
def info(self, ctxt, publisher_id, event_type, payload, metadata):
LOG.info("=== Event Received ===")
LOG.info("Event Type: " + str(event_type))
LOG.info("Payload: " + str(payload))
# TODO: Error Handling
for handler in self.handlers:
handler.handle(event_type, payload)
return NotificationResult.HANDLED

View file

@ -1,75 +0,0 @@
from django.db import transaction
from django.utils import timezone
from core.models import Invoice, InvoiceInstance, FlavorPrice
class ComputeHandler:
def get_tenant_progress_invoice(self, tenant_id):
return Invoice.objects.filter(project__tenant_id=tenant_id, state=Invoice.InvoiceState.IN_PROGRESS).first()
def handle(self, event_type, payload):
if event_type == 'compute.instance.update':
tenant_id = payload['tenant_id']
# Get instance invoice
invoice = self.get_tenant_progress_invoice(tenant_id)
if not invoice:
return
# print('New Compute State: ' + str(payload['state']))
if payload['state'] == 'active':
self.handle_active_state(invoice, payload)
if payload['state'] == 'deleted':
self.handle_delete_state(invoice, payload)
# TODO: Handle flavor change
@transaction.atomic
def handle_active_state(self, invoice, payload):
display_name = payload['display_name']
instance_id = payload['instance_id']
flavor_id = payload['instance_flavor_id']
state = payload['state']
# TODO: More filter if update is implemented
is_exists = InvoiceInstance.objects.filter(
invoice=invoice,
instance_id=instance_id,
flavor_id=flavor_id
).exists()
if not is_exists:
# Get Price
flavor_price = FlavorPrice.objects.filter(flavor_id=flavor_id).first()
# Create new invoice instance
InvoiceInstance.objects.create(
invoice=invoice,
instance_id=instance_id,
name=display_name,
flavor_id=flavor_id,
current_state=state,
start_date=timezone.now(),
daily_price=flavor_price.daily_price,
monthly_price=flavor_price.monthly_price,
)
@transaction.atomic
def handle_delete_state(self, invoice, payload):
instance_id = payload['instance_id']
flavor_id = payload['instance_flavor_id']
state = payload['state']
# TODO: More filter if update is implemented
invoice_instance = InvoiceInstance.objects.filter(
invoice=invoice,
instance_id=instance_id,
flavor_id=flavor_id
).first()
if invoice_instance:
invoice_instance.end_date = timezone.now()
invoice_instance.current_state = state
invoice_instance.save()

View file

@ -1,68 +0,0 @@
from django.db import transaction
from django.utils import timezone
from core.models import Invoice, InvoiceFloatingIp, FloatingIpsPrice
class NetworkHandler:
def get_tenant_progress_invoice(self, tenant_id):
return Invoice.objects.filter(project__tenant_id=tenant_id, state=Invoice.InvoiceState.IN_PROGRESS).first()
def handle(self, event_type, payload):
if event_type == 'floatingip.create.end':
tenant_id = payload['floatingip']['tenant_id']
# Get instance invoice
invoice = self.get_tenant_progress_invoice(tenant_id)
if not invoice:
return
self.handle_create(invoice, payload)
if event_type == 'floatingip.delete.end':
tenant_id = payload['floatingip']['tenant_id']
# Get instance invoice
invoice = self.get_tenant_progress_invoice(tenant_id)
if not invoice:
return
self.handle_delete(invoice, payload)
@transaction.atomic
def handle_create(self, invoice: Invoice, payload):
fip_id = payload['floatingip']['id']
ip = payload['floatingip']['floating_ip_address']
is_exists = InvoiceFloatingIp.objects.filter(
invoice=invoice,
fip_id=fip_id
).exists()
if not is_exists:
# Get Price
fip_price = FloatingIpsPrice.objects.first()
# Create new invoice floating ip
InvoiceFloatingIp.objects.create(
invoice=invoice,
fip_id=fip_id,
ip=ip,
current_state='allocated',
start_date=timezone.now(),
daily_price=fip_price.daily_price,
monthly_price=fip_price.monthly_price,
)
@transaction.atomic
def handle_delete(self, invoice: Invoice, payload):
fip_id = payload['floatingip']['id']
invoice_ip = InvoiceFloatingIp.objects.filter(
invoice=invoice,
fip_id=fip_id,
).first()
if invoice_ip:
invoice_ip.end_date = timezone.now()
invoice_ip.current_state = 'released'
invoice_ip.save()

View file

@ -1,77 +0,0 @@
from django.db import transaction
from django.utils import timezone
from core.models import Invoice, InvoiceVolume, VolumePrice
class VolumeHandler:
def get_tenant_progress_invoice(self, tenant_id):
return Invoice.objects.filter(project__tenant_id=tenant_id, state=Invoice.InvoiceState.IN_PROGRESS).first()
def handle(self, event_type, payload):
if event_type == 'volume.create.end':
tenant_id = payload['tenant_id']
# Get instance invoice
invoice = self.get_tenant_progress_invoice(tenant_id)
if not invoice:
return
self.handle_create(invoice, payload)
if event_type == 'volume.delete.end':
tenant_id = payload['tenant_id']
# Get instance invoice
invoice = self.get_tenant_progress_invoice(tenant_id)
if not invoice:
return
self.handle_delete(invoice, payload)
@transaction.atomic
def handle_create(self, invoice: Invoice, payload):
volume_id = payload['volume_id']
volume_type_id = payload['volume_type']
name = payload['display_name']
status = payload['status']
size = payload['size']
# TODO: More filter if update is implemented
is_exists = InvoiceVolume.objects.filter(
invoice=invoice,
volume_id=volume_id
).exists()
if not is_exists:
# Get Price
volume_price = VolumePrice.objects.filter(volume_type_id=volume_type_id).first()
# Create new invoice floating ip
InvoiceVolume.objects.create(
invoice=invoice,
volume_id=volume_id,
volume_name=name,
volume_type_id=volume_type_id,
space_allocation_gb=size,
current_state=status,
start_date=timezone.now(),
daily_price=volume_price.daily_price,
monthly_price=volume_price.monthly_price,
)
@transaction.atomic
def handle_delete(self, invoice: Invoice, payload):
volume_id = payload['volume_id']
status = payload['status']
# TODO: More filter if update is implemented
invoice_volume = InvoiceVolume.objects.filter(
invoice=invoice,
volume_id=volume_id
).first()
if invoice_volume:
invoice_volume.end_date = timezone.now()
invoice_volume.current_state = status
invoice_volume.save()

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

0
core/utils/__init__.py Normal file
View file

View file

@ -0,0 +1,63 @@
import json
from core.models import DynamicSetting
BILLING_ENABLED = "billing_enabled"
INVOICE_TAX = "invoice_tax"
DEFAULTS = {
BILLING_ENABLED: False,
INVOICE_TAX: 10
}
def _get_casted_value(setting: DynamicSetting):
if setting.type == DynamicSetting.DataType.JSON:
return json.loads(setting.value)
elif setting.type == DynamicSetting.DataType.BOOLEAN:
return setting.value == "1"
elif setting.type == DynamicSetting.DataType.INT:
return int(setting.value)
elif setting.type == DynamicSetting.DataType.STR:
return setting.value
else:
raise ValueError("Type not supported")
def get_dynamic_settings():
result = DEFAULTS.copy()
settings = DynamicSetting.objects.all()
for setting in settings:
result[setting.key] = _get_casted_value(setting)
return result
def get_dynamic_setting(key):
setting: DynamicSetting = DynamicSetting.objects.filter(key=key).first()
if not setting:
return DEFAULTS[key]
return setting.value
def set_dynamic_setting(key, value):
if type(value) is dict:
inserted_value = json.dumps(value)
data_type = DynamicSetting.DataType.JSON
elif type(value) is bool:
inserted_value = "1" if value else "0"
data_type = DynamicSetting.DataType.BOOLEAN
elif type(value) is int:
inserted_value = str(value)
data_type = DynamicSetting.DataType.INT
elif type(value) is str:
inserted_value = value
data_type = DynamicSetting.DataType.STR
else:
raise ValueError("Type not supported")
DynamicSetting.objects.update_or_create(key=key, defaults={
"value": inserted_value,
"type": data_type
})

88
core/utils/model_utils.py Normal file
View file

@ -0,0 +1,88 @@
import math
from django.db import models
from django.utils import timezone
from djmoney.models.fields import MoneyField
class BaseModel(models.Model):
class Meta:
abstract = True
class TimestampMixin(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class PriceMixin(models.Model):
hourly_price = MoneyField(max_digits=10, decimal_places=0)
monthly_price = MoneyField(max_digits=10, decimal_places=0, default=None, blank=True, null=True)
class Meta:
abstract = True
class InvoiceComponentMixin(TimestampMixin, PriceMixin):
"""
Storing start time for price calculation
"""
start_date = models.DateTimeField()
"""
Storing end time of the component, when component still active it will be None.
It will be set when component is closed or rolled
"""
end_date = models.DateTimeField(default=None, blank=True, null=True)
@property
def adjusted_end_date(self):
"""
Get end date that will be used for calculation. Please use this for calculation and displaying usage duration
Basically it just return current time if end_date is None
end_date will be set when invoice is finished every end of the month or when invoice component is rolled
"""
current_date = timezone.now()
if self.end_date:
end_date = self.end_date
else:
end_date = current_date
return end_date
@property
def price_charged(self):
"""
Calculate the price to be charged to user
"""
end_date = self.adjusted_end_date
if self.start_date.date().day == 1 and end_date.date().day == 1 \
and self.start_date.date().month != end_date.date().month \
and self.monthly_price:
# Using monthly price
return self.monthly_price
seconds_passes = (end_date - self.start_date).total_seconds()
hour_passes = math.ceil(seconds_passes / 3600)
return self.hourly_price * hour_passes
def is_closed(self):
"""
Is component closed
"""
return self.end_date is not None
def close(self, date):
"""
Close component the component
"""
self.end_date = date
self.save()
class Meta:
abstract = True

View file

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View file

@ -150,4 +150,5 @@ DEFAULT_CURRENCY = "IDR"
# Notification
RINTIK_NOTIFICATION_URL = "rabbit://openstack:HcmHchx2wbxZYjvbZUFkA5FObioWSkUY74DCpgvB@172.10.10.10:5672/"
RINTIK_NOTIFICATION_TOPICS = ["notifications", "cinder_notifications", "versioned_notifications"]
RINTIK_NOTIFICATION_TOPICS = ["notifications", "cinder_notifications", "versioned_notifications"]
RINTIK_INVOICE_TAX = 10