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(), linkgroups.LinkGroupDelete.as_view(),
name="linkgroup_delete", 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) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -268,8 +268,6 @@ class LinkGroupForm(RestrictedFormMixin, ModelForm):
model = LinkGroup model = LinkGroup
fields = ( fields = (
"name", "name",
"aggregators",
"platforms",
"platform_owner_cut_percentage", "platform_owner_cut_percentage",
"requisition_owner_cut_percentage", "requisition_owner_cut_percentage",
"operator_cut_percentage", "operator_cut_percentage",
@ -278,27 +276,12 @@ class LinkGroupForm(RestrictedFormMixin, ModelForm):
help_texts = { help_texts = {
"name": "The name of the link group.", "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.", "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.", "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.", "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.", "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): def clean(self):
cleaned_data = super(LinkGroupForm, self).clean() cleaned_data = super(LinkGroupForm, self).clean()
platform_owner_cut_percentage = cleaned_data.get( platform_owner_cut_percentage = cleaned_data.get(

View File

@ -69,6 +69,24 @@ class LinkGroup(models.Model):
def __str__(self): def __str__(self):
return self.name 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): class Aggregator(models.Model):
""" """

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
{% load cache %} {% load cache %}
{% load joinsep %}
{% load cachalot cache %} {% load cachalot cache %}
{% get_last_invalidation 'core.Platform' as last %} {% get_last_invalidation 'core.Platform' as last %}
{% include 'mixins/partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
@ -15,6 +16,8 @@
<th>user</th> <th>user</th>
<th>name</th> <th>name</th>
<th>service</th> <th>service</th>
<th>payees</th>
<th>link group</th>
<th>enabled</th> <th>enabled</th>
<th>actions</th> <th>actions</th>
</thead> </thead>
@ -32,6 +35,12 @@
<td>{{ item.user }}</td> <td>{{ item.user }}</td>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td>{{ item.get_service_display }}</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> <td>
{% if item.enabled %} {% if item.enabled %}
<span class="icon"> <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.clients.aggregators.nordigen import NordigenClient
from core.forms import AggregatorForm from core.forms import AggregatorForm
from core.models import Aggregator from core.models import Aggregator, Requisition
from core.util import logs from core.util import logs
from core.views.helpers import synchronize_async_helper from core.views.helpers import synchronize_async_helper
@ -95,6 +95,16 @@ class ReqsList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
run = synchronize_async_helper(NordigenClient(aggregator)) run = synchronize_async_helper(NordigenClient(aggregator))
reqs = synchronize_async_helper(run.get_requisitions()) 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 return reqs

View File

@ -1,9 +1,107 @@
from django.contrib.auth.mixins import LoginRequiredMixin 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 two_factor.views.mixins import OTPRequiredMixin
from core.forms import LinkGroupForm 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): class LinkGroupList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):