Begin implementing per-requisition configuration

This commit is contained in:
Mark Veidemanis 2023-03-16 20:20:36 +00:00
parent 4211d3c10a
commit 4d4406643f
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
14 changed files with 307 additions and 36 deletions

View File

@ -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/",

View File

@ -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

View File

@ -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:
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 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}**"

View File

@ -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.",
}

View File

@ -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):

View File

@ -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)),
],
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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)

View File

@ -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"

32
core/tests/helpers.py Normal file
View File

@ -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",
)

106
core/tests/test_platform.py Normal file
View File

@ -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,
)

View File

@ -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",

View File

@ -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)