Implement profit sharing system and write tests
This commit is contained in:
parent
8c490d6ee3
commit
9627fb7d41
|
@ -226,6 +226,11 @@ urlpatterns = [
|
|||
wallets.WalletUpdate.as_view(),
|
||||
name="wallet_update",
|
||||
),
|
||||
path(
|
||||
"operator_wallets/<str:type>/update/",
|
||||
wallets.OperatorWalletsUpdate.as_view(),
|
||||
name="operator_wallets_update",
|
||||
),
|
||||
path(
|
||||
"wallets/<str:type>/delete/<str:pk>/",
|
||||
wallets.WalletDelete.as_view(),
|
||||
|
|
|
@ -10,6 +10,7 @@ from .models import (
|
|||
Asset,
|
||||
LinkGroup,
|
||||
NotificationSettings,
|
||||
OperatorWallets,
|
||||
Platform,
|
||||
Provider,
|
||||
Requisition,
|
||||
|
@ -311,3 +312,19 @@ class LinkGroupForm(RestrictedFormMixin, ModelForm):
|
|||
)
|
||||
return
|
||||
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,
|
||||
)
|
||||
|
|
|
@ -10,7 +10,7 @@ from elasticsearch import AsyncElasticsearch
|
|||
from forex_python.converter import CurrencyRates
|
||||
|
||||
# Other library imports
|
||||
from core.models import Aggregator, Platform
|
||||
from core.models import Aggregator, OperatorWallets, Platform
|
||||
|
||||
# TODO: secure ES traffic properly
|
||||
urllib3.disable_warnings()
|
||||
|
@ -525,5 +525,67 @@ class Money(object):
|
|||
await self.write_to_es("get_total_with_trades", cast_es)
|
||||
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()
|
||||
|
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -260,6 +260,7 @@ class Platform(models.Model):
|
|||
)
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
throughput = models.FloatField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -584,6 +585,19 @@ class Requisition(models.Model):
|
|||
throughput = models.FloatField(default=0)
|
||||
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 = {
|
||||
"XMR": "Monero",
|
||||
|
|
|
@ -292,6 +292,9 @@
|
|||
<a class="navbar-item" href="{% url 'notifications_update' type='page' %}">
|
||||
Notifications
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'operator_wallets_update' type='page' %}">
|
||||
Operator Wallets
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -88,7 +88,10 @@
|
|||
</div>
|
||||
|
||||
<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="column">
|
||||
<div class="content">
|
||||
|
|
|
@ -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)
|
|
@ -101,6 +101,9 @@ class TestPlatform(AggregatorPlatformMixin, TransactionTestCase):
|
|||
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
|
||||
|
|
|
@ -2,8 +2,8 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
|||
from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
|
||||
from two_factor.views.mixins import OTPRequiredMixin
|
||||
|
||||
from core.forms import WalletForm
|
||||
from core.models import Wallet
|
||||
from core.forms import OperatorWalletsForm, WalletForm
|
||||
from core.models import OperatorWallets, Wallet
|
||||
|
||||
|
||||
class WalletList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
||||
|
@ -33,3 +33,21 @@ class WalletUpdate(LoginRequiredMixin, OTPRequiredMixin, ObjectUpdate):
|
|||
|
||||
class WalletDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete):
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue