Implement link group detail screen with profit simulation
This commit is contained in:
parent
bbd25c7450
commit
8c490d6ee3
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue