From 35607898f0aec48e0a4eb06a1818de2d7c68ef96 Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Fri, 5 May 2023 13:41:00 +0100 Subject: [PATCH] Begin implementing better payment simulation --- app/urls.py | 5 + core/lib/money.py | 31 ++++- .../partials/linkgroup-info-sim.html | 45 ++++++++ core/templates/partials/linkgroup-info.html | 45 +------- core/tests/test_money.py | 17 +++ core/views/linkgroups.py | 109 +++++++++++++++++- 6 files changed, 200 insertions(+), 52 deletions(-) create mode 100644 core/templates/partials/linkgroup-info-sim.html diff --git a/app/urls.py b/app/urls.py index 5c37927..964fe5b 100644 --- a/app/urls.py +++ b/app/urls.py @@ -267,4 +267,9 @@ urlpatterns = [ linkgroups.LinkGroupInfo.as_view(), name="linkgroup_info", ), + path( + "links//simulate//", + linkgroups.LinkGroupSimulation.as_view(), + name="linkgroup_simulate", + ), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/core/lib/money.py b/core/lib/money.py index efb144e..7e08bf2 100644 --- a/core/lib/money.py +++ b/core/lib/money.py @@ -47,6 +47,7 @@ class Money(object): """ Run all the balance checks that output into ES in another thread. """ + # TODO: pass link group instead if not all([user, nordigen, agora]): raise Exception @@ -559,10 +560,16 @@ class Money(object): # Add the platform payment for platform in platforms: # Get ratio of platform.throughput to the total platform throughput - ratio = platform.throughput / total_throughput_platform + if total_throughput_platform == 0: + ratio = 0 + else: + ratio = platform.throughput / total_throughput_platform platform_payment = cut_platform * ratio payees_length = len(platform.payees.all()) - payment_per_payee = platform_payment / payees_length + if payees_length == 0: + payment_per_payee = 0 + else: + payment_per_payee = platform_payment / payees_length for wallet in platform.payees.all(): if wallet not in pay_list: pay_list[wallet] = [] @@ -575,10 +582,16 @@ class Money(object): # Add the requisition payment for requisition in requisitions: # Get ratio of requisition.throughput to the requisition cut - ratio = requisition.throughput / total_throughput_requisition + if total_throughput_requisition == 0: + ratio = 0 + else: + ratio = requisition.throughput / total_throughput_requisition req_payment = cut_req * ratio payees_length = len(requisition.payees.all()) - payment_per_payee = req_payment / payees_length + if payees_length == 0: + payment_per_payee = 0 + else: + payment_per_payee = req_payment / payees_length for wallet in requisition.payees.all(): if wallet not in pay_list: pay_list[wallet] = [] @@ -590,5 +603,15 @@ class Money(object): return pay_list + def collapse_pay_list(self, pay_list): + """ + Collapse the pay list into a single dict of wallet: amount. + """ + collapsed = {} + for wallet, payments in pay_list.items(): + collapsed[wallet] = sum([x[0] for x in payments]) + + return collapsed + money = Money() diff --git a/core/templates/partials/linkgroup-info-sim.html b/core/templates/partials/linkgroup-info-sim.html new file mode 100644 index 0000000..2f5824c --- /dev/null +++ b/core/templates/partials/linkgroup-info-sim.html @@ -0,0 +1,45 @@ +

Simulation for $1000

+{{ object }} +

+ Assuming equal throughput for platforms and requisitions. + Note that this is just a simulation, equal throughput is highly unlikely. +

+
+
+
+
    + {% for key, list in simulation.items %} +
  • + {{ key.0 }}: ${{ key.1 }} +
      + {% for item in list %} +
    • ${{ item.amount }} to {{ item.name }} at {{ item.address }}
    • + {% endfor %} +
    +
  • + {% endfor %} +
+
+

Total for wallets

+
+ {% for wallet, pay_list in pay_list.items %} + {{ wallet }}: ${{ pay_list.amount }} + + {% endfor %} +
+
+ +
+
+ {% for key, list in simulation.items %} + {{ key.0 }}: ${{ key.1 }} + + {% for item in list %} + {{ item.name }}: ${{ item.amount }} + {% endfor %} + + {% endfor %} +
+
+ +
\ No newline at end of file diff --git a/core/templates/partials/linkgroup-info.html b/core/templates/partials/linkgroup-info.html index e39ce99..4d8335c 100644 --- a/core/templates/partials/linkgroup-info.html +++ b/core/templates/partials/linkgroup-info.html @@ -89,47 +89,4 @@ -

Simulation for $1000

-

- Assuming equal throughput for platforms and requisitions. - Note that this is just a simulation, equal throughput is highly unlikely. -

-
-
-
-
    - {% for key, list in simulation.items %} -
  • - {{ key.0 }}: ${{ key.1 }} -
      - {% for item in list %} -
    • ${{ item.amount }} to {{ item.name }} at {{ item.address }}
    • - {% endfor %} -
    -
  • - {% endfor %} -
-
-

Total for wallets

-
- {% for wallet, pay_list in pay_list.items %} - {{ wallet }}: ${{ pay_list.amount }} - - {% endfor %} -
-
- -
-
- {% for key, list in simulation.items %} - {{ key.0 }}: ${{ key.1 }} - - {% for item in list %} - {{ item.name }}: ${{ item.amount }} - {% endfor %} - - {% endfor %} -
-
- -
\ No newline at end of file +{% include 'partials/linkgroup-info-sim.html' %} \ No newline at end of file diff --git a/core/tests/test_money.py b/core/tests/test_money.py index 6364fe0..051ab24 100644 --- a/core/tests/test_money.py +++ b/core/tests/test_money.py @@ -237,3 +237,20 @@ class TestMoney(AggregatorPlatformMixin, TransactionTestCase): amount, expected_requisition[wallets_requisition.index(wallet)] ) self.assertIn("Requisition", detail) + + def test_collapse_pay_list(self): + self.create_wallets() + test_data = { + "WALLET1": [ + (500.0, "Operator cut for 2 of 1 operators, total 1000.0"), + (500.0, "Operator cut for 1 of 2 operators, total 1000.0"), + ], + "WALLET2": [(0.0, "Platform Live cut for 1 of 1 payees, total 500.0")], + } + expected = { + "WALLET1": 1000.0, + "WALLET2": 0.0, + } + + collapsed = money.collapse_pay_list(test_data) + self.assertDictEqual(collapsed, expected) diff --git a/core/views/linkgroups.py b/core/views/linkgroups.py index fffdd5a..4ed0c61 100644 --- a/core/views/linkgroups.py +++ b/core/views/linkgroups.py @@ -1,5 +1,6 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponse +from django.urls import reverse from mixins.views import ( ObjectCreate, ObjectDelete, @@ -10,8 +11,19 @@ from mixins.views import ( from rest_framework import status from two_factor.views.mixins import OTPRequiredMixin +from core.clients.aggregators.nordigen import NordigenClient +from core.clients.platforms.agora import AgoraClient from core.forms import LinkGroupForm -from core.models import Aggregator, LinkGroup, Platform, Requisition +from core.lib.money import Money +from core.models import ( + Aggregator, + LinkGroup, + OperatorWallets, + Platform, + Requisition, + User, +) +from core.views.helpers import synchronize_async_helper class LinkGroupInfo(LoginRequiredMixin, OTPRequiredMixin, ObjectRead): @@ -32,6 +44,25 @@ class LinkGroupInfo(LoginRequiredMixin, OTPRequiredMixin, ObjectRead): def get_context_data(self): context = super().get_context_data() + self.extra_buttons = [ + { + "url": "#", + "action": "withdraw", + "method": "get", + "label": "Withdraw profit", + "icon": "fa-solid fa-money-bill-transfer", + }, + { + "url": reverse( + "linkgroup_simulate", kwargs={"pk": self.object.id, "type": "modal"} + ), + "action": "simulate", + "method": "get", + "label": "Simulate withdrawal", + "icon": "fa-solid fa-play", + }, + ] + aggregators = Aggregator.objects.filter( user=self.request.user, link_group=self.object, @@ -50,8 +81,8 @@ class LinkGroupInfo(LoginRequiredMixin, OTPRequiredMixin, ObjectRead): context["linkgroup"] = self.object payees = self.object.payees() - simulation = {} + profit = 1000 profit_platform = profit * (self.object.platform_owner_cut_percentage / 100) profit_requisition = profit * ( @@ -95,12 +126,35 @@ class LinkGroupInfo(LoginRequiredMixin, OTPRequiredMixin, ObjectRead): pay_list[payee] = dict(cast) requisition_pay_list.append(cast) + operator_pay_list = [] + staff = User.objects.filter( + is_staff=True, + ) + for user in staff: + wallets, _ = OperatorWallets.objects.get_or_create(user=user) + total_wallets = len(wallets.payees.all()) + # Select all OperatorWallet instances with any distinct user attributes + for payee in wallets.payees.all(): + cast = { + "name": payee.name, + "address": payee.address, + "amount": profit_operator / total_wallets, + "max": profit_operator, + } + print("CAST", cast) + if user 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) + operator_pay_list.append(cast) + simulation[("Platform", profit_platform)] = platform_pay_list simulation[("Requisition", profit_requisition)] = requisition_pay_list - simulation[("Operator", profit_operator)] = [] + simulation[("Operator", profit_operator)] = operator_pay_list context["pay_list"] = pay_list - context["simulation"] = simulation return context @@ -134,3 +188,50 @@ class LinkGroupUpdate(LoginRequiredMixin, OTPRequiredMixin, ObjectUpdate): class LinkGroupDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete): model = LinkGroup + + +class LinkGroupSimulation(LoginRequiredMixin, OTPRequiredMixin, ObjectRead): + context_object_name_singular = "linkgroupsim" + context_object_name = "linkgroupsim" + detail_template = "partials/linkgroup-info-sim.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) + money = Money() + checks = synchronize_async_helper( + money.check_all( + user=self.request.user, nordigen=NordigenClient, agora=AgoraClient + ) + ) + print("CHECKS", checks) + + 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, + ) + + pay_list = money.get_pay_list( + linkgroup, + requisitions, + platforms, + self.request.user, + checks["total_profit"], + ) + print("PAY LIST", pay_list) + collapsed = money.collapse_pay_list(pay_list) + print("COLLAPSED", collapsed) + return collapsed