diff --git a/api/serializers.py b/api/serializers.py index 8570f40..e8e3a1c 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,3 +1,4 @@ +from djmoney.contrib.django_rest_framework import MoneyField from rest_framework import serializers from core.models import FlavorPrice, VolumePrice, FloatingIpsPrice, Invoice, InvoiceInstance, InvoiceVolume, \ @@ -23,18 +24,30 @@ class VolumePriceSerializer(serializers.ModelSerializer): class InvoiceInstanceSerializer(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__' 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__' @@ -44,6 +57,8 @@ 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") class Meta: model = Invoice @@ -53,5 +68,5 @@ class InvoiceSerializer(serializers.ModelSerializer): class SimpleInvoiceSerializer(serializers.ModelSerializer): class Meta: model = Invoice - fields = '__all__' + fields = ['id', 'start_date', 'end_date', 'state'] diff --git a/api/views.py b/api/views.py index 4bf42bc..665f52f 100644 --- a/api/views.py +++ b/api/views.py @@ -1,10 +1,13 @@ +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 from api.serializers import FlavorPriceSerializer, FloatingIpsPriceSerializer, VolumePriceSerializer, InvoiceSerializer, \ SimpleInvoiceSerializer -from core.models import FlavorPrice, FloatingIpsPrice, VolumePrice, Invoice +from core.models import FlavorPrice, FloatingIpsPrice, VolumePrice, Invoice, BillingProject, InvoiceInstance, \ + InvoiceFloatingIp class FlavorPriceViewSet(viewsets.ModelViewSet): @@ -27,15 +30,65 @@ class InvoiceViewSet(viewsets.ModelViewSet): def get_queryset(self): tenant_id = self.request.query_params.get('tenant_id', None) - return Invoice.objects.filter(project__tenant_id=tenant_id) + return Invoice.objects.filter(project__tenant_id=tenant_id).order_by('-start_date') @action(detail=False) - def simple_lists(self, request): + def simple_list(self, request): serializer = SimpleInvoiceSerializer(self.get_queryset(), many=True) return Response(serializer.data) @action(detail=False, methods=['POST']) def init_invoice(self, request): - # TODO: Init invoice - serializer = InvoiceSerializer() + project, created = BillingProject.objects.get_or_create(tenant_id=request.data['tenant_id']) + + 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() + + # Create Instance + for instance in request.data['instances']: + # Get Price + flavor_price = FlavorPrice.objects.filter(flavor_id=instance['flavor_id']).first() + + # Create new invoice instance + start_date = dateutil.parser.isoparse(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, + ) + + for fip in request.data['floating_ips']: + # Get Price + fip_price = FloatingIpsPrice.objects.first() + + # Create new invoice floating ip + start_date = dateutil.parser.isoparse(fip['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, + ) + + serializer = InvoiceSerializer(new_invoice) + return Response(serializer.data) diff --git a/core/migrations/0008_auto_20210901_0636.py b/core/migrations/0008_auto_20210901_0636.py new file mode 100644 index 0000000..867341c --- /dev/null +++ b/core/migrations/0008_auto_20210901_0636.py @@ -0,0 +1,23 @@ +# 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), + ), + ] diff --git a/core/migrations/0009_auto_20210901_0644.py b/core/migrations/0009_auto_20210901_0644.py new file mode 100644 index 0000000..727d8e6 --- /dev/null +++ b/core/migrations/0009_auto_20210901_0644.py @@ -0,0 +1,23 @@ +# 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), + ), + ] diff --git a/core/models.py b/core/models.py index 89cab69..68ece58 100644 --- a/core/models.py +++ b/core/models.py @@ -1,9 +1,10 @@ -from calendar import monthrange from datetime import timedelta +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 class FlavorPrice(models.Model): @@ -44,11 +45,14 @@ class Invoice(models.Model): @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 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 + if price == 0: + return Money(amount=price, currency=settings.DEFAULT_CURRENCY) - # TODO: Add other price - return instance_price + return instance_price + fip_price + volume_price class InvoiceInstance(models.Model): @@ -65,16 +69,20 @@ class InvoiceInstance(models.Model): updated_at = models.DateTimeField(auto_now=True, blank=True, null=True) @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 - + 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 # TODO: For Testing, please delete end_date += timedelta(days=1) @@ -88,12 +96,33 @@ class InvoiceFloatingIp(models.Model): ip = models.CharField(max_length=256) current_state = models.CharField(max_length=256) start_date = models.DateTimeField() - end_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_created=True) + 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 + # TODO: For Testing, please delete + 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') @@ -102,8 +131,29 @@ class InvoiceVolume(models.Model): space_allocation_gb = models.IntegerField() current_state = models.CharField(max_length=256) start_date = models.DateTimeField() - end_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_created=True) + 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 + # TODO: For Testing, please delete + end_date += timedelta(days=1) + + days = (end_date - self.start_date).days + return self.daily_price * self.space_allocation_gb * days diff --git a/core/notification_endpoint.py b/core/notification_endpoint.py index 18e8960..5ecdc65 100644 --- a/core/notification_endpoint.py +++ b/core/notification_endpoint.py @@ -3,17 +3,26 @@ 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)) + LOG.info("=== Event Received ===") + LOG.info("Event Type: " + str(event_type)) + LOG.info("Payload: " + str(payload)) - ComputeHandler().handle(event_type, payload) + # TODO: Error Handling + for handler in self.handlers: + handler.handle(event_type, payload) - return NotificationResult.HANDLED \ No newline at end of file + return NotificationResult.HANDLED diff --git a/core/notification_handler/compute_handler.py b/core/notification_handler/compute_handler.py index ae2e9ff..3e8f083 100644 --- a/core/notification_handler/compute_handler.py +++ b/core/notification_handler/compute_handler.py @@ -1,3 +1,4 @@ +from django.db import transaction from django.utils import timezone from core.models import Invoice, InvoiceInstance, FlavorPrice @@ -5,7 +6,7 @@ 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() + 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': @@ -25,6 +26,7 @@ class ComputeHandler: # TODO: Handle flavor change + @transaction.atomic def handle_active_state(self, invoice, payload): display_name = payload['display_name'] instance_id = payload['instance_id'] @@ -53,6 +55,7 @@ class ComputeHandler: 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'] diff --git a/core/notification_handler/network_handler.py b/core/notification_handler/network_handler.py index 0e90862..2caba63 100644 --- a/core/notification_handler/network_handler.py +++ b/core/notification_handler/network_handler.py @@ -1,2 +1,68 @@ +from django.db import transaction +from django.utils import timezone + +from core.models import Invoice, InvoiceFloatingIp, FloatingIpsPrice + + class NetworkHandler: - pass \ No newline at end of file + 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 = FloatingIpsPrice.objects.filter( + invoice=invoice, + fip_id=fip_id, + ).first() + + if invoice_ip: + invoice_ip.end_date = timezone.now() + invoice_ip.state = 'released' + invoice_ip.save() diff --git a/core/notification_handler/volume_handler.py b/core/notification_handler/volume_handler.py new file mode 100644 index 0000000..629cc40 --- /dev/null +++ b/core/notification_handler/volume_handler.py @@ -0,0 +1,3 @@ +class VolumeHandler: + def handle(self, event_type, payload): + pass