invoicing

This commit is contained in:
Setyo Nugroho 2021-09-01 19:29:24 +07:00
parent a6c0f1d67a
commit 34db325ffe
9 changed files with 270 additions and 25 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
return NotificationResult.HANDLED

View file

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

View file

@ -1,2 +1,68 @@
from django.db import transaction
from django.utils import timezone
from core.models import Invoice, InvoiceFloatingIp, FloatingIpsPrice
class NetworkHandler:
pass
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()

View file

@ -0,0 +1,3 @@
class VolumeHandler:
def handle(self, event_type, payload):
pass