Implement profit sharing system and write tests

This commit is contained in:
Mark Veidemanis 2023-03-20 11:06:37 +00:00
parent 8c490d6ee3
commit 9627fb7d41
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
11 changed files with 411 additions and 4 deletions

View File

@ -226,6 +226,11 @@ urlpatterns = [
wallets.WalletUpdate.as_view(), wallets.WalletUpdate.as_view(),
name="wallet_update", name="wallet_update",
), ),
path(
"operator_wallets/<str:type>/update/",
wallets.OperatorWalletsUpdate.as_view(),
name="operator_wallets_update",
),
path( path(
"wallets/<str:type>/delete/<str:pk>/", "wallets/<str:type>/delete/<str:pk>/",
wallets.WalletDelete.as_view(), wallets.WalletDelete.as_view(),

View File

@ -10,6 +10,7 @@ from .models import (
Asset, Asset,
LinkGroup, LinkGroup,
NotificationSettings, NotificationSettings,
OperatorWallets,
Platform, Platform,
Provider, Provider,
Requisition, Requisition,
@ -311,3 +312,19 @@ class LinkGroupForm(RestrictedFormMixin, ModelForm):
) )
return return
return cleaned_data return cleaned_data
class OperatorWalletsForm(RestrictedFormMixin, ModelForm):
class Meta:
model = OperatorWallets
fields = ("payees",)
help_texts = {
"payees": "Wallets to designate as payees for this operator.",
}
payees = forms.ModelMultipleChoiceField(
queryset=Wallet.objects.all(),
widget=forms.CheckboxSelectMultiple,
help_text=Meta.help_texts["payees"],
required=False,
)

View File

@ -10,7 +10,7 @@ from elasticsearch import AsyncElasticsearch
from forex_python.converter import CurrencyRates from forex_python.converter import CurrencyRates
# Other library imports # Other library imports
from core.models import Aggregator, Platform from core.models import Aggregator, OperatorWallets, Platform
# TODO: secure ES traffic properly # TODO: secure ES traffic properly
urllib3.disable_warnings() urllib3.disable_warnings()
@ -525,5 +525,67 @@ class Money(object):
await self.write_to_es("get_total_with_trades", cast_es) await self.write_to_es("get_total_with_trades", cast_es)
return total_with_trades return total_with_trades
def get_pay_list(self, linkgroup, requisitions, platforms, user, profit):
pay_list = {} # Wallet: [(amount, reason), (amount, reason), ...]
# Get the total amount of money we have
total_throughput_platform = 0
total_throughput_requisition = 0
for requisition in requisitions:
total_throughput_requisition += requisition.throughput
for platform in platforms:
total_throughput_platform += platform.throughput
cut_platform = profit * (linkgroup.platform_owner_cut_percentage / 100)
cut_req = profit * (linkgroup.requisition_owner_cut_percentage / 100)
cut_operator = profit * (linkgroup.operator_cut_percentage / 100)
# Add the operator payment
operator_wallets = OperatorWallets.objects.filter(user=user).first()
operator_length = len(operator_wallets.payees.all())
payment_per_operator = cut_operator / operator_length
for wallet in operator_wallets.payees.all():
if wallet not in pay_list:
pay_list[wallet] = []
detail = (
f"Operator cut for 1 of {operator_length} operators, total "
f"{cut_operator}"
)
pay_list[wallet].append((payment_per_operator, detail))
# Add the platform payment
for platform in platforms:
# Get ratio of platform.throughput to the total platform throughput
ratio = platform.throughput / total_throughput_platform
platform_payment = cut_platform * ratio
payees_length = len(platform.payees.all())
payment_per_payee = platform_payment / payees_length
for wallet in platform.payees.all():
if wallet not in pay_list:
pay_list[wallet] = []
detail = (
f"Platform {platform} cut for 1 of {payees_length} payees, "
f"total {cut_platform}"
)
pay_list[wallet].append((payment_per_payee, detail))
# Add the requisition payment
for requisition in requisitions:
# Get ratio of requisition.throughput to the requisition cut
ratio = requisition.throughput / total_throughput_requisition
req_payment = cut_req * ratio
payees_length = len(requisition.payees.all())
payment_per_payee = req_payment / payees_length
for wallet in requisition.payees.all():
if wallet not in pay_list:
pay_list[wallet] = []
detail = (
f"Requisition {requisition} cut for 1 of {payees_length} payees, "
f"total {cut_req}"
)
pay_list[wallet].append((payment_per_payee, detail))
return pay_list
money = Money() money = Money()

View File

@ -0,0 +1,25 @@
# Generated by Django 4.1.7 on 2023-03-20 09:35
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0031_remove_ad_aggregators_remove_ad_platforms_and_more'),
]
operations = [
migrations.CreateModel(
name='OperatorWallets',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('payees', models.ManyToManyField(blank=True, to='core.wallet')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.7 on 2023-03-20 09:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0032_operatorwallets'),
]
operations = [
migrations.AddField(
model_name='platform',
name='throughput',
field=models.FloatField(default=0),
),
]

View File

@ -260,6 +260,7 @@ class Platform(models.Model):
) )
enabled = models.BooleanField(default=True) enabled = models.BooleanField(default=True)
throughput = models.FloatField(default=0)
def __str__(self): def __str__(self):
return self.name return self.name
@ -584,6 +585,19 @@ class Requisition(models.Model):
throughput = models.FloatField(default=0) throughput = models.FloatField(default=0)
payees = models.ManyToManyField(Wallet, blank=True) payees = models.ManyToManyField(Wallet, blank=True)
def __str__(self):
return f"Aggregator: {self.aggregator.name} ID: {self.requisition_id}"
class OperatorWallets(models.Model):
"""
A list of wallets to designate as operator wallets for this user.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE)
payees = models.ManyToManyField(Wallet, blank=True)
assets = { assets = {
"XMR": "Monero", "XMR": "Monero",

View File

@ -292,6 +292,9 @@
<a class="navbar-item" href="{% url 'notifications_update' type='page' %}"> <a class="navbar-item" href="{% url 'notifications_update' type='page' %}">
Notifications Notifications
</a> </a>
<a class="navbar-item" href="{% url 'operator_wallets_update' type='page' %}">
Operator Wallets
</a>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@ -88,7 +88,10 @@
</div> </div>
<h1 class="title">Simulation for $1000</h1> <h1 class="title">Simulation for $1000</h1>
<p>Assuming equal throughput for platforms and requisitions.</p> <p>
Assuming equal throughput for platforms and requisitions.
<strong>Note that this is just a simulation, equal throughput is highly unlikely.</strong>
</p>
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<div class="content"> <div class="content">

239
core/tests/test_money.py Normal file
View File

@ -0,0 +1,239 @@
from django.test import TransactionTestCase
from core.clients.platform import LocalPlatformClient
from core.lib.money import money
from core.models import LinkGroup, OperatorWallets, Platform, Requisition, Wallet
from core.tests.helpers import AggregatorPlatformMixin
class TestMoney(AggregatorPlatformMixin, TransactionTestCase):
def setUp(self):
super().setUp()
self.plat_client = LocalPlatformClient()
self.plat_client.instance = self.platform
self.linkgroup = LinkGroup.objects.create(
user=self.user,
name="Test",
platform_owner_cut_percentage=30,
requisition_owner_cut_percentage=40,
operator_cut_percentage=30,
)
self.aggregator.link_group = self.linkgroup
self.aggregator.save()
self.platform.link_group = self.linkgroup
self.platform.save()
self.req = Requisition.objects.create(
user=self.user,
aggregator=self.aggregator,
requisition_id="3ba3e65d-f44c-4c4e-9e28-08cc080830f6",
payment_details="CUSTOM PAYMENT",
)
def create_wallets(self):
self.wallet_1 = Wallet.objects.create(
user=self.user,
name="Platform wallet 1",
address="alpha",
)
self.wallet_2 = Wallet.objects.create(
user=self.user,
name="Requisition wallet 1",
address="beta",
)
self.wallet_3 = Wallet.objects.create(
user=self.user,
name="Operator wallet 1",
address="gamma",
)
self.platform.payees.set([self.wallet_1])
self.req.payees.set([self.wallet_2])
op, _ = OperatorWallets.objects.get_or_create(user=self.user)
op.payees.set([self.wallet_3])
self.platform.save()
self.req.save()
op.save()
# Platform: 30
# Requisition: 40
# Operator: 30
def test_get_payment_list_full(self):
self.create_wallets()
self.platform.throughput = 1000
self.req.throughput = 1000
profit = 100
pay_list = money.get_pay_list(
self.linkgroup,
[self.req],
[self.platform],
self.user,
profit,
)
expected_list = {
self.wallet_3: [(30.0, "Operator cut for 1 of 1 operators, total 30.0")],
self.wallet_1: [(30.0, "Platform Test cut for 1 of 1 payees, total 30.0")],
self.wallet_2: [
(
40.0,
(
f"Requisition Aggregator: {self.aggregator.name} ID: "
f"{self.req.requisition_id} cut for 1 of 1 payees, total 40.0"
),
)
],
}
self.assertDictEqual(pay_list, expected_list)
def test_get_payment_list_full_duplicates(self):
self.create_wallets()
self.req.payees.set([self.wallet_1])
self.platform.throughput = 1000
self.req.throughput = 1000
profit = 100
pay_list = money.get_pay_list(
self.linkgroup,
[self.req],
[self.platform],
self.user,
profit,
)
expected_list = {
self.wallet_3: [(30.0, "Operator cut for 1 of 1 operators, total 30.0")],
self.wallet_1: [
(30.0, "Platform Test cut for 1 of 1 payees, total 30.0"),
(
40.0,
(
f"Requisition Aggregator: {self.aggregator.name} ID: "
f"{self.req.requisition_id} cut for 1 of 1 payees, total 40.0"
),
),
],
}
self.assertDictEqual(pay_list, expected_list)
def create_wallets_for_operator(self, num):
wallets = []
for i in range(num):
wallet = Wallet.objects.create(
user=self.user,
name=f"Operator wallet {i}",
address=f"operator{i}",
)
wallets.append(wallet)
op, _ = OperatorWallets.objects.get_or_create(user=self.user)
op.payees.set(wallets)
return wallets
def create_platforms_and_wallets(self, num):
platforms = []
wallets = []
for i in range(num):
platform = Platform.objects.create(
user=self.user,
name=f"Platform {i}",
service="agora",
token="a",
password="a",
otp_token="a",
username="myuser",
link_group=self.linkgroup,
)
wallet = Wallet.objects.create(
user=self.user,
name=f"Platform wallet {i}",
address=f"platform{i}",
)
platform.payees.set([wallet])
platforms.append(platform)
wallets.append(wallet)
return platforms, wallets
def create_requisitions_and_wallets(self, num):
requisitions = []
wallets = []
for i in range(num):
req = Requisition.objects.create(
user=self.user,
aggregator=self.aggregator,
requisition_id=f"3ba3e65d-f44c-4c4e-9e28-08cc080830f6{i}",
)
wallet = Wallet.objects.create(
user=self.user,
name=f"Requisition wallet {i}",
address=f"requisition{i}",
)
req.payees.set([wallet])
requisitions.append(req)
wallets.append(wallet)
return requisitions, wallets
def test_get_payment_list_multi(self):
wallet_num_operator = 3
wallet_num_platform = 3
wallet_num_requisition = 3
wallets_operator = self.create_wallets_for_operator(wallet_num_operator)
platforms, wallets_platforms = self.create_platforms_and_wallets(
wallet_num_platform
)
requisitions, wallets_requisition = self.create_requisitions_and_wallets(
wallet_num_requisition
)
profit = 100
throughput_platform = [400, 300, 300]
throughput_requisition = [300, 400, 300]
expected_operator = [10, 10, 10]
expected_platform = [12, 9, 9]
expected_requisition = [12, 16, 12]
for index, platform in enumerate(platforms):
platform.throughput = throughput_platform[index]
for index, req in enumerate(requisitions):
req.throughput = throughput_requisition[index]
pay_list = money.get_pay_list(
self.linkgroup,
requisitions,
platforms,
self.user,
profit,
)
for wallet, payments in pay_list.items():
if wallet in wallets_operator:
for amount, detail in payments:
self.assertEqual(
amount, expected_operator[wallets_operator.index(wallet)]
)
self.assertIn("Operator cut", detail)
elif wallet in wallets_platforms:
for amount, detail in payments:
self.assertEqual(
amount, expected_platform[wallets_platforms.index(wallet)]
)
self.assertIn("Platform", detail)
elif wallet in wallets_requisition:
for amount, detail in payments:
self.assertEqual(
amount, expected_requisition[wallets_requisition.index(wallet)]
)
self.assertIn("Requisition", detail)

View File

@ -101,6 +101,9 @@ class TestPlatform(AggregatorPlatformMixin, TransactionTestCase):
self.linkgroup = LinkGroup.objects.create( self.linkgroup = LinkGroup.objects.create(
user=self.user, user=self.user,
name="Test", name="Test",
platform_owner_cut_percentage=30,
requisition_owner_cut_percentage=40,
operator_cut_percentage=30,
) )
self.aggregator.link_group = self.linkgroup self.aggregator.link_group = self.linkgroup

View File

@ -2,8 +2,8 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
from two_factor.views.mixins import OTPRequiredMixin from two_factor.views.mixins import OTPRequiredMixin
from core.forms import WalletForm from core.forms import OperatorWalletsForm, WalletForm
from core.models import Wallet from core.models import OperatorWallets, Wallet
class WalletList(LoginRequiredMixin, OTPRequiredMixin, ObjectList): class WalletList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
@ -33,3 +33,21 @@ class WalletUpdate(LoginRequiredMixin, OTPRequiredMixin, ObjectUpdate):
class WalletDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete): class WalletDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete):
model = Wallet model = Wallet
class OperatorWalletsUpdate(LoginRequiredMixin, ObjectUpdate):
model = OperatorWallets
form_class = OperatorWalletsForm
page_title = "Update your designated wallets."
submit_url_name = "operator_wallets_update"
submit_url_args = ["type"]
pk_required = False
hide_cancel = True
def get_object(self, **kwargs):
wallet, _ = OperatorWallets.objects.get_or_create(user=self.request.user)
return wallet