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(),
|
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(),
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
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",
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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(
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue