Begin implementing per-requisition configuration
This commit is contained in:
parent
4211d3c10a
commit
4d4406643f
|
@ -110,6 +110,11 @@ urlpatterns = [
|
||||||
banks.BanksCurrencies.as_view(),
|
banks.BanksCurrencies.as_view(),
|
||||||
name="currencies",
|
name="currencies",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"banks/<str:type>/req/<str:aggregator_id>/<str:req_id>/",
|
||||||
|
banks.BanksRequisitionUpdate.as_view(),
|
||||||
|
name="requisition_update",
|
||||||
|
),
|
||||||
# Bank balances
|
# Bank balances
|
||||||
path(
|
path(
|
||||||
"banks/<str:type>/balances/",
|
"banks/<str:type>/balances/",
|
||||||
|
|
|
@ -19,6 +19,7 @@ class AggregatorClient(ABC):
|
||||||
# #if account["account_id"] in self.banks
|
# #if account["account_id"] in self.banks
|
||||||
# }
|
# }
|
||||||
# For each bank
|
# For each bank
|
||||||
|
print("ACCOUNT INFOS", account_infos)
|
||||||
for bank, accounts in account_infos.items():
|
for bank, accounts in account_infos.items():
|
||||||
# Iterate the accounts
|
# Iterate the accounts
|
||||||
for index, account in enumerate(list(accounts)):
|
for index, account in enumerate(list(accounts)):
|
||||||
|
@ -50,6 +51,8 @@ class AggregatorClient(ABC):
|
||||||
self.instance.currencies = currencies
|
self.instance.currencies = currencies
|
||||||
self.instance.save()
|
self.instance.save()
|
||||||
|
|
||||||
|
print("INSTANCE ACCOUNT INFO", self.instance.account_info)
|
||||||
|
|
||||||
async def process_transactions(self, account_id, transactions):
|
async def process_transactions(self, account_id, transactions):
|
||||||
if not transactions:
|
if not transactions:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -1104,6 +1104,12 @@ class LocalPlatformClient(ABC):
|
||||||
currency_account_info_map[currency]["recipient"] = account[
|
currency_account_info_map[currency]["recipient"] = account[
|
||||||
"ownerName"
|
"ownerName"
|
||||||
]
|
]
|
||||||
|
currency_account_info_map[currency]["aggregator_id"] = account[
|
||||||
|
"aggregator_id"
|
||||||
|
]
|
||||||
|
currency_account_info_map[currency]["requisition_id"] = account[
|
||||||
|
"requisition_id"
|
||||||
|
]
|
||||||
return (list(currency_account_info_map.keys()), currency_account_info_map)
|
return (list(currency_account_info_map.keys()), currency_account_info_map)
|
||||||
|
|
||||||
def get_matching_account_details(self, currency, ad):
|
def get_matching_account_details(self, currency, ad):
|
||||||
|
@ -1200,12 +1206,21 @@ class LocalPlatformClient(ABC):
|
||||||
if not payment_details:
|
if not payment_details:
|
||||||
return False
|
return False
|
||||||
if real:
|
if real:
|
||||||
payment = ad.payment_details_real
|
aggregator_id = payment_details["aggregator_id"]
|
||||||
|
requisition_id = payment_details["requisition_id"]
|
||||||
|
req = self.instance.get_requisition(aggregator_id, requisition_id)
|
||||||
|
if req:
|
||||||
|
payment = req.payment_details
|
||||||
|
else:
|
||||||
|
payment = ad.payment_details_real
|
||||||
else:
|
else:
|
||||||
payment = ad.payment_details
|
payment = ad.payment_details
|
||||||
|
|
||||||
payment_text = ""
|
payment_text = ""
|
||||||
for field, value in payment_details.items():
|
for field, value in payment_details.items():
|
||||||
|
if field in ["aggregator_id", "requisition_id"]:
|
||||||
|
# Don't send these to the user
|
||||||
|
continue
|
||||||
formatted_name = field.replace("_", " ")
|
formatted_name = field.replace("_", " ")
|
||||||
formatted_name = formatted_name.capitalize()
|
formatted_name = formatted_name.capitalize()
|
||||||
payment_text += f"* {formatted_name}: **{value}**"
|
payment_text += f"* {formatted_name}: **{value}**"
|
||||||
|
|
|
@ -11,6 +11,7 @@ from .models import (
|
||||||
NotificationSettings,
|
NotificationSettings,
|
||||||
Platform,
|
Platform,
|
||||||
Provider,
|
Provider,
|
||||||
|
Requisition,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -205,3 +206,17 @@ class AdForm(RestrictedFormMixin, ModelForm):
|
||||||
help_text=Meta.help_texts["aggregators"],
|
help_text=Meta.help_texts["aggregators"],
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RequisitionForm(RestrictedFormMixin, ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Requisition
|
||||||
|
fields = (
|
||||||
|
"payment_details",
|
||||||
|
"transaction_source",
|
||||||
|
)
|
||||||
|
|
||||||
|
help_texts = {
|
||||||
|
"payment_details": "Shown once a user opens a trade.",
|
||||||
|
"transaction_source": "Whether to check pending or booked transactions.",
|
||||||
|
}
|
||||||
|
|
|
@ -171,7 +171,7 @@ class TransactionsCurrencyExchange(MyModel):
|
||||||
|
|
||||||
|
|
||||||
class TXAccount(MyModel):
|
class TXAccount(MyModel):
|
||||||
iban: str
|
iban: str | None
|
||||||
bban: str | None
|
bban: str | None
|
||||||
|
|
||||||
|
|
||||||
|
@ -191,6 +191,7 @@ class TransactionsNested(MyModel):
|
||||||
proprietaryBankTransactionCode: str | None
|
proprietaryBankTransactionCode: str | None
|
||||||
internalTransactionId: str | None
|
internalTransactionId: str | None
|
||||||
currencyExchange: TransactionsCurrencyExchange | None
|
currencyExchange: TransactionsCurrencyExchange | None
|
||||||
|
merchantCategoryCode: str | None
|
||||||
|
|
||||||
|
|
||||||
class TransactionsBookedPending(MyModel):
|
class TransactionsBookedPending(MyModel):
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Generated by Django 4.1.7 on 2023-03-15 10:14
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0024_ad_send_reference'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Requisition',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('requisition_id', models.CharField(max_length=255)),
|
||||||
|
('payment_details', models.TextField()),
|
||||||
|
('transaction_source', models.CharField(choices=[('booked', 'Booked'), ('pending', 'Pending')], max_length=255)),
|
||||||
|
('aggregator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.aggregator')),
|
||||||
|
('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-15 10:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0025_requisition'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='requisition',
|
||||||
|
name='transaction_source',
|
||||||
|
field=models.CharField(choices=[('booked', 'Booked'), ('pending', 'Pending')], default='booked', max_length=255),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.1.7 on 2023-03-15 10:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0026_alter_requisition_transaction_source'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='requisition',
|
||||||
|
name='payment_details',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -363,6 +363,16 @@ class Platform(models.Model):
|
||||||
|
|
||||||
return aggregators
|
return aggregators
|
||||||
|
|
||||||
|
def get_requisition(self, aggregator_id, requisition_id):
|
||||||
|
"""
|
||||||
|
Get a Requisition object with the provided values.
|
||||||
|
"""
|
||||||
|
requisition = Requisition.objects.filter(
|
||||||
|
aggregator_id=aggregator_id,
|
||||||
|
requisition_id=requisition_id,
|
||||||
|
).first()
|
||||||
|
return requisition
|
||||||
|
|
||||||
|
|
||||||
class Asset(models.Model):
|
class Asset(models.Model):
|
||||||
code = models.CharField(max_length=64)
|
code = models.CharField(max_length=64)
|
||||||
|
@ -493,12 +503,13 @@ class Requisition(models.Model):
|
||||||
A requisition for an Aggregator
|
A requisition for an Aggregator
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
aggregator = models.ForeignKey(Aggregator, on_delete=models.CASCADE)
|
aggregator = models.ForeignKey(Aggregator, on_delete=models.CASCADE)
|
||||||
requisition_id = models.CharField(max_length=255)
|
requisition_id = models.CharField(max_length=255)
|
||||||
payment_details = models.TextField()
|
payment_details = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
transaction_source = models.CharField(
|
transaction_source = models.CharField(
|
||||||
max_length=255, choices=TRANSACTION_SOURCE_CHOICES
|
max_length=255, choices=TRANSACTION_SOURCE_CHOICES, default="booked"
|
||||||
)
|
)
|
||||||
|
|
||||||
# throughput = models.FloatField(default=0)
|
# throughput = models.FloatField(default=0)
|
||||||
|
|
|
@ -7,7 +7,18 @@
|
||||||
{# cache 600 objects_banks_currencies request.user.id object_list type last #}
|
{# cache 600 objects_banks_currencies request.user.id object_list type last #}
|
||||||
|
|
||||||
{% for bank, accounts in object_list.items %}
|
{% for bank, accounts in object_list.items %}
|
||||||
<h1 class="title is-4">{{ bank.0 }} <code>{{ bank.1 }}</code></h1>
|
<h1 class="title is-4">{{ bank.0 }} <code>{{ bank.1 }}</code>
|
||||||
|
<a
|
||||||
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||||
|
hx-get="{% url 'requisition_update' type=type aggregator_id=bank.2 req_id=bank.1 %}"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#{{ type }}s-here"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
<span class="icon has-text-black" data-tooltip="Configure">
|
||||||
|
<i class="fa-solid fa-wrench"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
<table
|
<table
|
||||||
class="table is-fullwidth is-hoverable"
|
class="table is-fullwidth is-hoverable"
|
||||||
hx-target="#{{ bank }}-table"
|
hx-target="#{{ bank }}-table"
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from core.clients.aggregator import AggregatorClient
|
||||||
|
from core.models import Aggregator, Platform, User
|
||||||
|
|
||||||
|
|
||||||
|
class AggregatorPlatformMixin:
|
||||||
|
def setUp(self):
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username="testuser", email="test@example.com", password="test"
|
||||||
|
)
|
||||||
|
self.aggregator = Aggregator.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
name="Test",
|
||||||
|
service="nordigen",
|
||||||
|
secret_id="a",
|
||||||
|
secret_key="a",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.agg_client = AggregatorClient()
|
||||||
|
self.agg_client.instance = self.aggregator
|
||||||
|
|
||||||
|
self.platform = Platform.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
name="Test",
|
||||||
|
service="agora",
|
||||||
|
token="a",
|
||||||
|
password="a",
|
||||||
|
otp_token="a",
|
||||||
|
username="myuser",
|
||||||
|
)
|
|
@ -0,0 +1,106 @@
|
||||||
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
|
from core.clients.platforms.agora import AgoraClient
|
||||||
|
from core.models import Ad, Asset, Provider
|
||||||
|
from core.tests.helpers import AggregatorPlatformMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TestPlatform(AggregatorPlatformMixin, TransactionTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.aggregator.account_info = {
|
||||||
|
"MONZO_MONZGB2L": [
|
||||||
|
{
|
||||||
|
"resourceId": "ssss",
|
||||||
|
"currency": "GBP",
|
||||||
|
"ownerName": "JSNOW LIMITED",
|
||||||
|
"cashAccountType": "CACC",
|
||||||
|
"status": "enabled",
|
||||||
|
"maskedPan": None,
|
||||||
|
"details": "Private Limited Company",
|
||||||
|
"bban": "ssss",
|
||||||
|
"name": None,
|
||||||
|
"product": None,
|
||||||
|
"bic": None,
|
||||||
|
"recipient": "TODO",
|
||||||
|
"account_id": "s-s-s-s-s",
|
||||||
|
"aggregator_id": str(self.aggregator.id),
|
||||||
|
"requisition_id": "3ba3e65d-f44c-4c4e-9e28-08cc080830f6",
|
||||||
|
"account_number": {"sort_code": "04-00-04", "number": "00000002"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceId": "ssss",
|
||||||
|
"currency": "GBP",
|
||||||
|
"ownerName": "John Snow Smith",
|
||||||
|
"cashAccountType": "CACC",
|
||||||
|
"status": "enabled",
|
||||||
|
"maskedPan": None,
|
||||||
|
"details": "Personal Account",
|
||||||
|
"bban": "ssss",
|
||||||
|
"name": None,
|
||||||
|
"product": None,
|
||||||
|
"bic": None,
|
||||||
|
"recipient": "TODO",
|
||||||
|
"account_id": "s-s-s-s-s",
|
||||||
|
"aggregator_id": str(self.aggregator.id),
|
||||||
|
"requisition_id": "3ba3e65d-f44c-4c4e-9e28-08cc080830f6",
|
||||||
|
"account_number": {"sort_code": "04-00-04", "number": "00000001"},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
self.aggregator.save()
|
||||||
|
|
||||||
|
self.plat_client = AgoraClient(self.platform)
|
||||||
|
|
||||||
|
asset = Asset.objects.create(
|
||||||
|
code="XMR",
|
||||||
|
name="Monero",
|
||||||
|
)
|
||||||
|
|
||||||
|
provider = Provider.objects.create(
|
||||||
|
code="REVOLUT",
|
||||||
|
name="Revolut",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.ad = Ad.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
name="Test",
|
||||||
|
text="Ad text",
|
||||||
|
payment_details="Payment details",
|
||||||
|
payment_details_real="Payment details real",
|
||||||
|
payment_method_details="Payment method details",
|
||||||
|
dist_list="",
|
||||||
|
asset_list=[asset],
|
||||||
|
provider_list=[provider],
|
||||||
|
platforms=[self.platform],
|
||||||
|
aggregators=[self.aggregator],
|
||||||
|
send_reference=True,
|
||||||
|
visible=True,
|
||||||
|
enabled=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_valid_account_details(self):
|
||||||
|
result = self.plat_client.get_valid_account_details(self.ad)
|
||||||
|
|
||||||
|
def test_get_matching_account_details(self):
|
||||||
|
result = self.plat_client.get_matching_account_details("GBP", self.ad)
|
||||||
|
|
||||||
|
def test_format_payment_details(self):
|
||||||
|
account_info = self.plat_client.get_matching_account_details("GBP", self.ad)
|
||||||
|
result = self.plat_client.format_payment_details(
|
||||||
|
"GBP",
|
||||||
|
account_info,
|
||||||
|
self.ad,
|
||||||
|
real=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_format_payment_details_real(self):
|
||||||
|
account_info = self.plat_client.get_matching_account_details("GBP", self.ad)
|
||||||
|
result = self.plat_client.format_payment_details(
|
||||||
|
"GBP",
|
||||||
|
account_info,
|
||||||
|
self.ad,
|
||||||
|
real=True,
|
||||||
|
)
|
|
@ -1,38 +1,14 @@
|
||||||
import logging
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
from core.clients.aggregator import AggregatorClient
|
from core.models import Trade, Transaction
|
||||||
from core.models import Aggregator, Platform, Trade, Transaction, User
|
from core.tests.helpers import AggregatorPlatformMixin
|
||||||
|
|
||||||
|
|
||||||
class TestTransactions(TransactionTestCase):
|
class TestTransactions(AggregatorPlatformMixin, TransactionTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
logging.disable(logging.CRITICAL)
|
super().setUp()
|
||||||
self.user = User.objects.create_user(
|
|
||||||
username="testuser", email="test@example.com", password="test"
|
|
||||||
)
|
|
||||||
self.aggregator = Aggregator.objects.create(
|
|
||||||
user=self.user,
|
|
||||||
name="Test",
|
|
||||||
service="nordigen",
|
|
||||||
secret_id="a",
|
|
||||||
secret_key="a",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.agg_client = AggregatorClient()
|
|
||||||
self.agg_client.instance = self.aggregator
|
|
||||||
|
|
||||||
self.platform = Platform.objects.create(
|
|
||||||
user=self.user,
|
|
||||||
name="Test",
|
|
||||||
service="agora",
|
|
||||||
token="a",
|
|
||||||
password="a",
|
|
||||||
otp_token="a",
|
|
||||||
username="myuser",
|
|
||||||
)
|
|
||||||
self.transaction = Transaction.objects.create(
|
self.transaction = Transaction.objects.create(
|
||||||
aggregator=self.aggregator,
|
aggregator=self.aggregator,
|
||||||
account_id="my account id",
|
account_id="my account id",
|
||||||
|
|
|
@ -1,16 +1,50 @@
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.http import HttpResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from mixins.views import ObjectList
|
from mixins.views import ObjectList, ObjectUpdate
|
||||||
|
from rest_framework import status
|
||||||
from two_factor.views.mixins import OTPRequiredMixin
|
from two_factor.views.mixins import OTPRequiredMixin
|
||||||
|
|
||||||
from core.clients.aggregators.nordigen import NordigenClient
|
from core.clients.aggregators.nordigen import NordigenClient
|
||||||
from core.models import Aggregator
|
from core.forms import RequisitionForm
|
||||||
|
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
|
||||||
|
|
||||||
log = logs.get_logger(__name__)
|
log = logs.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BanksRequisitionUpdate(LoginRequiredMixin, OTPRequiredMixin, ObjectUpdate):
|
||||||
|
model = Requisition
|
||||||
|
form_class = RequisitionForm
|
||||||
|
|
||||||
|
page_title = "Update settings for requisition"
|
||||||
|
|
||||||
|
submit_url_name = "requisition_update"
|
||||||
|
submit_url_args = ["type", "aggregator_id", "req_id"]
|
||||||
|
|
||||||
|
pk_required = False
|
||||||
|
|
||||||
|
hide_cancel = True
|
||||||
|
|
||||||
|
def get_object(self, **kwargs):
|
||||||
|
aggregator_id = self.kwargs["aggregator_id"]
|
||||||
|
req_id = self.kwargs["req_id"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
aggregator = Aggregator.objects.get(
|
||||||
|
user=self.request.user, pk=aggregator_id
|
||||||
|
)
|
||||||
|
except Aggregator.DoesNotExist:
|
||||||
|
return HttpResponse(status=status.HTTP_404_NOT_FOUND)
|
||||||
|
req, _ = Requisition.objects.get_or_create(
|
||||||
|
user=self.request.user,
|
||||||
|
aggregator=aggregator,
|
||||||
|
requisition_id=req_id,
|
||||||
|
)
|
||||||
|
return req
|
||||||
|
|
||||||
|
|
||||||
class BanksCurrencies(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
class BanksCurrencies(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
||||||
"""
|
"""
|
||||||
Get a list of bank accounts with their details.
|
Get a list of bank accounts with their details.
|
||||||
|
@ -45,7 +79,7 @@ class BanksCurrencies(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
||||||
for agg in aggregators:
|
for agg in aggregators:
|
||||||
for bank, accounts in agg.account_info.items():
|
for bank, accounts in agg.account_info.items():
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
ident = (bank, account["requisition_id"])
|
ident = (bank, account["requisition_id"], account["aggregator_id"])
|
||||||
if ident not in account_info:
|
if ident not in account_info:
|
||||||
account_info[ident] = []
|
account_info[ident] = []
|
||||||
account_info[ident].append(account)
|
account_info[ident].append(account)
|
||||||
|
|
Loading…
Reference in New Issue