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(),
name="currencies",
),
path(
"banks/<str:type>/req/<str:aggregator_id>/<str:req_id>/",
banks.BanksRequisitionUpdate.as_view(),
name="requisition_update",
),
# Bank balances
path(
"banks/<str:type>/balances/",

View File

@ -19,6 +19,7 @@ class AggregatorClient(ABC):
# #if account["account_id"] in self.banks
# }
# For each bank
print("ACCOUNT INFOS", account_infos)
for bank, accounts in account_infos.items():
# Iterate the accounts
for index, account in enumerate(list(accounts)):
@ -50,6 +51,8 @@ class AggregatorClient(ABC):
self.instance.currencies = currencies
self.instance.save()
print("INSTANCE ACCOUNT INFO", self.instance.account_info)
async def process_transactions(self, account_id, transactions):
if not transactions:
return False

View File

@ -1104,6 +1104,12 @@ class LocalPlatformClient(ABC):
currency_account_info_map[currency]["recipient"] = account[
"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)
def get_matching_account_details(self, currency, ad):
@ -1200,12 +1206,21 @@ class LocalPlatformClient(ABC):
if not payment_details:
return False
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
else:
payment = ad.payment_details
payment_text = ""
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 = formatted_name.capitalize()
payment_text += f"* {formatted_name}: **{value}**"

View File

@ -11,6 +11,7 @@ from .models import (
NotificationSettings,
Platform,
Provider,
Requisition,
User,
)
@ -205,3 +206,17 @@ class AdForm(RestrictedFormMixin, ModelForm):
help_text=Meta.help_texts["aggregators"],
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):
iban: str
iban: str | None
bban: str | None
@ -191,6 +191,7 @@ class TransactionsNested(MyModel):
proprietaryBankTransactionCode: str | None
internalTransactionId: str | None
currencyExchange: TransactionsCurrencyExchange | None
merchantCategoryCode: str | None
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
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):
code = models.CharField(max_length=64)
@ -493,12 +503,13 @@ class Requisition(models.Model):
A requisition for an Aggregator
"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
aggregator = models.ForeignKey(Aggregator, on_delete=models.CASCADE)
requisition_id = models.CharField(max_length=255)
payment_details = models.TextField()
payment_details = models.TextField(null=True, blank=True)
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)

View File

@ -7,7 +7,18 @@
{# cache 600 objects_banks_currencies request.user.id object_list type last #}
{% 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
class="table is-fullwidth is-hoverable"
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 django.test import TransactionTestCase
from core.clients.aggregator import AggregatorClient
from core.models import Aggregator, Platform, Trade, Transaction, User
from core.models import Trade, Transaction
from core.tests.helpers import AggregatorPlatformMixin
class TestTransactions(TransactionTestCase):
class TestTransactions(AggregatorPlatformMixin, TransactionTestCase):
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",
)
super().setUp()
self.transaction = Transaction.objects.create(
aggregator=self.aggregator,
account_id="my account id",

View File

@ -1,16 +1,50 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse
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 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.views.helpers import synchronize_async_helper
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):
"""
Get a list of bank accounts with their details.
@ -45,7 +79,7 @@ class BanksCurrencies(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
for agg in aggregators:
for bank, accounts in agg.account_info.items():
for account in accounts:
ident = (bank, account["requisition_id"])
ident = (bank, account["requisition_id"], account["aggregator_id"])
if ident not in account_info:
account_info[ident] = []
account_info[ident].append(account)