Implement link group detail screen with profit simulation

This commit is contained in:
Mark Veidemanis 2023-03-18 14:06:50 +00:00
parent bbd25c7450
commit 8c490d6ee3
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
11 changed files with 292 additions and 21 deletions

View File

@ -252,4 +252,9 @@ urlpatterns = [
linkgroups.LinkGroupDelete.as_view(),
name="linkgroup_delete",
),
path(
"links/<str:type>/info/<str:pk>/",
linkgroups.LinkGroupInfo.as_view(),
name="linkgroup_info",
),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -268,8 +268,6 @@ class LinkGroupForm(RestrictedFormMixin, ModelForm):
model = LinkGroup
fields = (
"name",
"aggregators",
"platforms",
"platform_owner_cut_percentage",
"requisition_owner_cut_percentage",
"operator_cut_percentage",
@ -278,27 +276,12 @@ class LinkGroupForm(RestrictedFormMixin, ModelForm):
help_texts = {
"name": "The name of the link group.",
"aggregators": "The aggregators to use.",
"platforms": "The platforms to use.",
"platform_owner_cut_percentage": "The percentage of the total profit of this group to give to the platform owners.",
"requisition_owner_cut_percentage": "The percentage of the total profit of this group to give to the requisition owners.",
"operator_cut_percentage": "The percentage of the total profit of this group to give to the operator.",
"enabled": "Whether or not this link group is enabled.",
}
platforms = forms.ModelMultipleChoiceField(
queryset=Platform.objects.all(),
widget=forms.CheckboxSelectMultiple,
help_text=Meta.help_texts["platforms"],
required=True,
)
aggregators = forms.ModelMultipleChoiceField(
queryset=Aggregator.objects.all(),
widget=forms.CheckboxSelectMultiple,
help_text=Meta.help_texts["aggregators"],
required=True,
)
def clean(self):
cleaned_data = super(LinkGroupForm, self).clean()
platform_owner_cut_percentage = cleaned_data.get(

View File

@ -69,6 +69,24 @@ class LinkGroup(models.Model):
def __str__(self):
return self.name
def payees(self):
payees = {}
for platform in self.platform_set.all():
for payee in platform.payees.all():
if "platform" not in payees:
payees["platform"] = []
payees["platform"].append(payee)
for aggregator in self.aggregator_set.all():
agg_reqs = aggregator.requisition_set.all()
for req in agg_reqs:
for payee in req.payees.all():
if "requisition" not in payees:
payees["requisition"] = []
payees["requisition"].append(payee)
return payees
class Aggregator(models.Model):
"""

View File

@ -173,6 +173,7 @@
padding-right: 5px;
padding-left: 5px;
}
.grid-stack-item:hover .ui-resizable-handle {
display: block !important;
}
@ -360,7 +361,7 @@
{% block outer_content %}
{% endblock %}
<section class="section">
<div class="container">
<div class="container is-widescreen">
{% block content_wrapper %}
{% block content %}
{% endblock %}

View File

@ -10,6 +10,7 @@
<th>created</th>
<th>institution</th>
<th>accounts</th>
<th>payees</th>
<th>actions</th>
</thead>
{% for item in object_list %}
@ -26,6 +27,11 @@
<td>{{ item.created }}</td>
<td>{{ item.institution_id }}</td>
<td>{{ item.accounts|length }}</td>
<td>
{% for payee in item.requisition.payees.all %}
{{ payee.name }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</td>
<td>
<div class="buttons">
<button

View File

@ -15,6 +15,7 @@
<th>user</th>
<th>name</th>
<th>service</th>
<th>link group</th>
<th>enabled</th>
<th>actions</th>
</thead>
@ -32,6 +33,7 @@
<td>{{ item.user }}</td>
<td><a href="{% url 'reqs' type='page' pk=item.id %}">{{ item.name }}</a></td>
<td>{{ item.get_service_display }}</td>
<td>{{ item.link_group|default_if_none:"—" }}</td>
<td>
{% if item.enabled %}
<span class="icon">

View File

@ -0,0 +1,130 @@
<h1 class="title">Information for link group {{ linkgroup.name }}</h1>
<div class="columns">
<div class="column">
<h1 class="title is-4">Platforms</h1>
{% include 'partials/platform-list.html' with object_list=platforms type=type %}
<h1 class="title is-4">Aggregators</h1>
{% include 'partials/aggregator-list.html' with object_list=aggregators type=type %}
<h1 class="title is-4">Requisitions</h1>
<table class="table is-fullwidth is-hoverable">
<thead>
<th>id</th>
<th>aggregator</th>
<th>payees</th>
</thead>
{% for item in requisitions %}
<tr>
<td>{{ item.id|truncatechars:20 }}</td>
<td>{{ item.aggregator.name }}</td>
<td>
{% for payee in item.payees.all %}
{{ payee.name }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</td>
</tr>
{% endfor %}
</table>
</div>
<div class="column">
<h1 class="title is-4">Platform payees</h1>
<table class="table is-fullwidth is-hoverable">
<thead>
<th>name</th>
<th>address</th>
</thead>
{% for item in linkgroup.payees.platform %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.address }}</td>
</tr>
{% endfor %}
</table>
<h1 class="title is-4">Requisition payees</h1>
<table class="table is-fullwidth is-hoverable">
<thead>
<th>name</th>
<th>address</th>
</thead>
{% for item in linkgroup.payees.requisition %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.address }}</td>
</tr>
{% endfor %}
</table>
<h1 class="title is-4">Split</h1>
<table class="table is-fullwidth is-hoverable">
<thead>
<th>attribute</th>
<th>%</th>
<th>graphic</th>
</thead>
<tr>
<td>platform</td>
<td>{{ linkgroup.platform_owner_cut_percentage }}</td>
<td>
<progress class="progress" value="{{ linkgroup.platform_owner_cut_percentage }}" max="100"></progress>
</td>
</tr>
<tr>
<td>requisition</td>
<td>{{ linkgroup.requisition_owner_cut_percentage }}</td>
<td>
<progress class="progress" value="{{ linkgroup.requisition_owner_cut_percentage }}" max="100"></progress>
</td>
</tr>
<tr>
<td>operator</td>
<td>{{ linkgroup.operator_cut_percentage }}</td>
<td>
<progress class="progress" value="{{ linkgroup.operator_cut_percentage }}" max="100"></progress>
</td>
</tr>
</table>
</div>
</div>
<h1 class="title">Simulation for $1000</h1>
<p>Assuming equal throughput for platforms and requisitions.</p>
<div class="columns">
<div class="column">
<div class="content">
<ul>
{% for key, list in simulation.items %}
<li>
{{ key.0 }}: ${{ key.1 }}
<ul>
{% for item in list %}
<li>${{ item.amount }} to {{ item.name }} at <code>{{ item.address }}</code></li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</div>
<h1 class="title is-4">Total for wallets</h1>
<div class="box">
{% for wallet, pay_list in pay_list.items %}
{{ wallet }}: ${{ pay_list.amount }}
<progress class="progress" value="{{ pay_list.amount }}" max="1000"></progress>
{% endfor %}
</div>
</div>
<div class="column">
<div class="box">
{% for key, list in simulation.items %}
<strong>{{ key.0 }}: ${{ key.1 }}</strong>
<progress class="progress" value="{{ key.1 }}" max="1000"></progress>
{% for item in list %}
<em>{{ item.name }}: ${{ item.amount }}</em><progress class="progress" value="{{ item.amount }}" max="{{ item.max }}"></progress>
{% endfor %}
{% endfor %}
</div>
</div>
</div>

View File

@ -76,6 +76,15 @@
</span>
</span>
</button>
<a href="{% url 'linkgroup_info' type='page' pk=item.id %}"><button
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
</div>
</td>
</tr>

View File

@ -1,4 +1,5 @@
{% load cache %}
{% load joinsep %}
{% load cachalot cache %}
{% get_last_invalidation 'core.Platform' as last %}
{% include 'mixins/partials/notify.html' %}
@ -15,6 +16,8 @@
<th>user</th>
<th>name</th>
<th>service</th>
<th>payees</th>
<th>link group</th>
<th>enabled</th>
<th>actions</th>
</thead>
@ -32,6 +35,12 @@
<td>{{ item.user }}</td>
<td>{{ item.name }}</td>
<td>{{ item.get_service_display }}</td>
<td>
{% for payee in item.payees.all %}
{{ payee.name }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</td>
<td>{{ item.link_group|default_if_none:"—" }}</td>
<td>
{% if item.enabled %}
<span class="icon">

View File

@ -14,7 +14,7 @@ from two_factor.views.mixins import OTPRequiredMixin
from core.clients.aggregators.nordigen import NordigenClient
from core.forms import AggregatorForm
from core.models import Aggregator
from core.models import Aggregator, Requisition
from core.util import logs
from core.views.helpers import synchronize_async_helper
@ -95,6 +95,16 @@ class ReqsList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
run = synchronize_async_helper(NordigenClient(aggregator))
reqs = synchronize_async_helper(run.get_requisitions())
for req in reqs:
# Add in Requisition object
requisition_id = req["id"]
requisition = Requisition.objects.filter(
user=self.request.user,
aggregator=aggregator,
requisition_id=requisition_id,
).first()
if requisition:
req["requisition"] = requisition
return reqs

View File

@ -1,9 +1,107 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
from django.http import HttpResponse
from mixins.views import (
ObjectCreate,
ObjectDelete,
ObjectList,
ObjectRead,
ObjectUpdate,
)
from rest_framework import status
from two_factor.views.mixins import OTPRequiredMixin
from core.forms import LinkGroupForm
from core.models import LinkGroup
from core.models import Aggregator, LinkGroup, Platform, Requisition
class LinkGroupInfo(LoginRequiredMixin, OTPRequiredMixin, ObjectRead):
context_object_name_singular = "linkgroup"
context_object_name = "linkgroups"
detail_template = "partials/linkgroup-info.html"
def get_object(self, **kwargs):
pk = self.kwargs.get("pk")
linkgroup = LinkGroup.objects.filter(
user=self.request.user,
id=pk,
).first()
if not linkgroup:
return HttpResponse(status=status.HTTP_404_NOT_FOUND)
return linkgroup
def get_context_data(self):
context = super().get_context_data()
aggregators = Aggregator.objects.filter(
user=self.request.user,
link_group=self.object,
)
platforms = Platform.objects.filter(
user=self.request.user,
link_group=self.object,
)
requisitions = Requisition.objects.filter(
user=self.request.user,
aggregator__in=aggregators,
)
context["aggregators"] = aggregators
context["platforms"] = platforms
context["requisitions"] = requisitions
context["linkgroup"] = self.object
payees = self.object.payees()
simulation = {}
profit = 1000
profit_platform = profit * (self.object.platform_owner_cut_percentage / 100)
profit_requisition = profit * (
self.object.requisition_owner_cut_percentage / 100
)
profit_operator = profit * (self.object.operator_cut_percentage / 100)
pay_list = {}
platform_pay_list = []
for payee in payees["platform"]:
cast = {
"name": payee.name,
"address": payee.address,
"amount": profit_platform / len(payees["platform"]),
"max": profit_platform,
}
if payee not in pay_list:
pay_list[payee] = {}
if "amount" in pay_list[payee]:
pay_list[payee]["amount"] += cast["amount"]
else:
pay_list[payee] = dict(cast)
platform_pay_list.append(cast)
requisition_pay_list = []
for payee in payees["requisition"]:
cast = {
"name": payee.name,
"address": payee.address,
"amount": profit_requisition / len(payees["requisition"]),
"max": profit_requisition,
}
if payee not in pay_list:
pay_list[payee] = {}
if "amount" in pay_list[payee]:
pay_list[payee]["amount"] += cast["amount"]
else:
pay_list[payee] = dict(cast)
requisition_pay_list.append(cast)
simulation[("Platform", profit_platform)] = platform_pay_list
simulation[("Requisition", profit_requisition)] = requisition_pay_list
simulation[("Operator", profit_operator)] = []
context["pay_list"] = pay_list
context["simulation"] = simulation
return context
class LinkGroupList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):