Fetch account details and display
This commit is contained in:
parent
de04f8d29b
commit
bcfa8f61e1
14
app/urls.py
14
app/urls.py
|
@ -20,7 +20,7 @@ from django.contrib.auth.views import LogoutView
|
|||
from django.urls import include, path
|
||||
from two_factor.urls import urlpatterns as tf_urls
|
||||
|
||||
from core.views import aggregators, base, notifications
|
||||
from core.views import aggregators, banks, base, notifications
|
||||
|
||||
# from core.views.stripe_callbacks import Callback
|
||||
|
||||
|
@ -93,4 +93,16 @@ urlpatterns = [
|
|||
aggregators.ReqInfo.as_view(),
|
||||
name="req_info",
|
||||
),
|
||||
# Request bank fetch
|
||||
path(
|
||||
"ops/bank_fetch/<str:pk>/",
|
||||
aggregators.RequestBankFetch.as_view(),
|
||||
name="bank_fetch",
|
||||
),
|
||||
# Bank details by currency
|
||||
path(
|
||||
"banks/<str:type>/details/",
|
||||
banks.BanksCurrencies.as_view(),
|
||||
name="currencies",
|
||||
),
|
||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
from abc import ABC
|
||||
|
||||
|
||||
class AggregatorClient(ABC):
|
||||
def store_account_info(self, account_infos):
|
||||
print("STORE ACCOUNT INFO CALLED")
|
||||
# account_infos = {
|
||||
# bank: accounts
|
||||
# for bank, accounts in account_info.items()
|
||||
# for account in accounts
|
||||
# #if account["account_id"] in self.banks
|
||||
# }
|
||||
# For each bank
|
||||
for bank, accounts in account_infos.items():
|
||||
# Iterate the accounts
|
||||
for index, account in enumerate(list(accounts)):
|
||||
if "account_number" not in account:
|
||||
account_infos[bank][index]["account_number"] = {}
|
||||
fields = ["sort_code", "number", "iban"]
|
||||
for field in fields:
|
||||
if field in account:
|
||||
account_infos[bank][index]["account_number"][
|
||||
field
|
||||
] = account[field]
|
||||
del account_infos[bank][index][field]
|
||||
# if len(account["account_number"]) == 1:
|
||||
# account_infos[bank].remove(account)
|
||||
currencies = [
|
||||
account["currency"]
|
||||
for bank, accounts in account_infos.items()
|
||||
for account in accounts
|
||||
]
|
||||
for bank, accounts in account_infos.items():
|
||||
if not self.instance.account_info:
|
||||
self.instance.account_info = {}
|
||||
self.instance.account_info[bank] = []
|
||||
for account in accounts:
|
||||
self.instance.account_info[bank].append(account)
|
||||
# self.account_info = account_infos
|
||||
self.currencies = currencies
|
||||
|
||||
self.instance.currencies = currencies
|
||||
self.instance.save()
|
||||
|
||||
print("CURRENCIES", self.instance.currencies)
|
||||
print("ACCOUNT INFO", self.instance.account_info)
|
|
@ -3,13 +3,14 @@ from datetime import timedelta
|
|||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
from core.clients.aggregator import AggregatorClient
|
||||
from core.clients.base import BaseClient
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("nordigen")
|
||||
|
||||
|
||||
class NordigenClient(BaseClient):
|
||||
class NordigenClient(BaseClient, AggregatorClient):
|
||||
url = "https://ob.nordigen.com/api/v2"
|
||||
|
||||
async def connect(self):
|
||||
|
@ -60,7 +61,7 @@ class NordigenClient(BaseClient):
|
|||
"""
|
||||
# This function is a stub.
|
||||
|
||||
return ["GB", "SE"]
|
||||
return ["GB", "SE", "BG", "UA"]
|
||||
|
||||
async def get_banks(self, country):
|
||||
"""
|
||||
|
@ -128,28 +129,31 @@ class NordigenClient(BaseClient):
|
|||
|
||||
path = f"accounts/{account_id}/details"
|
||||
response = await self.call(path, schema="AccountDetails")
|
||||
print("RESPONSE", response)
|
||||
if "account" not in response:
|
||||
return False
|
||||
parsed = response["account"]
|
||||
if "bban" in parsed and parsed["currency"] == "GBP":
|
||||
sort_code = parsed["bban"][0:6]
|
||||
account_number = parsed["bban"][6:]
|
||||
del parsed["bban"]
|
||||
if "iban" in parsed:
|
||||
if "iban" in parsed and parsed["currency"] == "GBP":
|
||||
if parsed["iban"]:
|
||||
sort_code = parsed["iban"][-14:-8]
|
||||
account_number = parsed["iban"][-8:]
|
||||
del parsed["iban"]
|
||||
sort_code = "-".join(list(map("".join, zip(*[iter(sort_code)] * 2))))
|
||||
parsed["sort_code"] = sort_code
|
||||
parsed["number"] = account_number
|
||||
parsed["recipient"] = "TODO"
|
||||
# if "iban" in parsed:
|
||||
# del parsed["iban"]
|
||||
sort_code = "-".join(list(map("".join, zip(*[iter(sort_code)] * 2))))
|
||||
parsed["sort_code"] = sort_code
|
||||
parsed["number"] = account_number
|
||||
parsed["recipient"] = "TODO"
|
||||
# Let's add the account ID so we can reference it later
|
||||
parsed["account_id"] = account_id
|
||||
return parsed
|
||||
|
||||
async def get_all_account_info(self, requisition=None):
|
||||
async def get_all_account_info(self, requisition=None, store=False):
|
||||
to_return = {}
|
||||
if not requisition:
|
||||
raise NotImplementedError
|
||||
# requisitions = await self.get_requisitions()
|
||||
print("NOT REQUISITION")
|
||||
requisitions = await self.get_requisitions()
|
||||
print("GOT REQS", requisitions)
|
||||
else:
|
||||
requisitions = [await self.get_requisition(requisition)]
|
||||
|
||||
|
@ -163,4 +167,8 @@ class NordigenClient(BaseClient):
|
|||
to_return[req["institution_id"]].append(account_info)
|
||||
else:
|
||||
to_return[req["institution_id"]] = [account_info]
|
||||
|
||||
if store:
|
||||
print("TO STORE IS TRUE")
|
||||
self.store_account_info(to_return)
|
||||
return to_return
|
||||
|
|
|
@ -113,9 +113,12 @@ class AccountDetailsNested(BaseModel):
|
|||
currency: str
|
||||
ownerName: str
|
||||
cashAccountType: str
|
||||
status: str
|
||||
status: str | None
|
||||
maskedPan: str | None
|
||||
details: str
|
||||
details: str | None
|
||||
iban: str | None
|
||||
name: str | None
|
||||
product: str | None
|
||||
|
||||
|
||||
class AccountDetails(BaseModel):
|
||||
|
|
|
@ -3,6 +3,8 @@ import asyncio
|
|||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from core.clients.aggregators.nordigen import NordigenClient
|
||||
from core.models import Aggregator
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("scheduling")
|
||||
|
@ -12,6 +14,17 @@ INTERVAL = 5
|
|||
|
||||
async def job():
|
||||
print("Running schedule.")
|
||||
aggregators = Aggregator.objects.filter(enabled=True, fetch_accounts=True)
|
||||
for aggregator in aggregators:
|
||||
if aggregator.service == "nordigen":
|
||||
instance = await NordigenClient(aggregator)
|
||||
print("RUNNING GET ALL ACCOUNT INFO")
|
||||
await instance.get_all_account_info(store=True)
|
||||
print("FINISHED RUNNING")
|
||||
else:
|
||||
raise NotImplementedError(f"No such client library: {aggregator.service}")
|
||||
aggregator.fetch_accounts = False
|
||||
aggregator.save()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-09 11:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_aggregator_access_token_expires'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='aggregator',
|
||||
name='account_info',
|
||||
field=models.JSONField(default=list),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='aggregator',
|
||||
name='currencies',
|
||||
field=models.JSONField(default=list),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-09 11:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_aggregator_account_info_aggregator_currencies'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='aggregator',
|
||||
name='fetch_accounts',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.1.7 on 2023-03-09 14:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0006_aggregator_fetch_accounts'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='aggregator',
|
||||
name='account_info',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='aggregator',
|
||||
name='fetch_accounts',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
|
@ -45,6 +45,11 @@ class Aggregator(models.Model):
|
|||
access_token_expires = models.DateTimeField(null=True, blank=True)
|
||||
poll_interval = models.IntegerField(default=10)
|
||||
|
||||
account_info = models.JSONField(default=dict)
|
||||
currencies = models.JSONField(default=list)
|
||||
|
||||
fetch_accounts = models.BooleanField(default=True)
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
@ -219,6 +219,40 @@
|
|||
Home
|
||||
</a>
|
||||
{% if user.is_authenticated %}
|
||||
<a class="navbar-item" href="#">
|
||||
Profit
|
||||
</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Banks
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'currencies' type='page' %}">
|
||||
Currencies
|
||||
</a>
|
||||
<a class="navbar-item" href="#">
|
||||
Balances
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Platforms
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="#">
|
||||
Advert Configuration
|
||||
</a>
|
||||
<a class="navbar-item" href="#">
|
||||
Advert Management
|
||||
</a>
|
||||
<a class="navbar-item" href="#">
|
||||
Market Research
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Setup
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% load nsep %}
|
||||
|
||||
{% get_last_invalidation 'core.Aggregator' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{# 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 }}</h1>
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ bank }}-table"
|
||||
id="{{ bank }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>currency</th>
|
||||
<th>owner</th>
|
||||
<th>details</th>
|
||||
<th>payment</th>
|
||||
<th>id</th>
|
||||
|
||||
</thead>
|
||||
{% for account in accounts %}
|
||||
<tr>
|
||||
<td>{{ account.currency }}</td>
|
||||
<td>{{ account.ownerName }}</td>
|
||||
<td>{{ account.details|default_if_none:"—" }}</td>
|
||||
<td>
|
||||
{% for item in account.account_number.values %}
|
||||
<code>{{ item|default_if_none:"—" }}</code>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
class="has-text-grey"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.account_id }}/');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endfor %}
|
||||
{# endcache #}
|
|
@ -1,7 +1,7 @@
|
|||
import asyncio
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.views import View
|
||||
from mixins.views import (
|
||||
ObjectCreate,
|
||||
|
@ -16,25 +16,36 @@ from core.clients.aggregators.nordigen import NordigenClient
|
|||
from core.forms import AggregatorForm
|
||||
from core.models import Aggregator
|
||||
from core.util import logs
|
||||
from core.views.helpers import synchronize_async_helper
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
|
||||
|
||||
def synchronize_async_helper(to_await):
|
||||
async_response = []
|
||||
class RequestBankFetch(LoginRequiredMixin, OTPRequiredMixin, View):
|
||||
template_name = "mixins/partials/notify.html"
|
||||
|
||||
async def run_and_capture_result():
|
||||
r = await to_await
|
||||
async_response.append(r)
|
||||
def get(self, request, pk=None):
|
||||
if pk:
|
||||
try:
|
||||
aggregator = Aggregator.get_by_id(pk, self.request.user)
|
||||
aggregators = [aggregator]
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
coroutine = run_and_capture_result()
|
||||
loop.run_until_complete(coroutine)
|
||||
return async_response[0]
|
||||
except Aggregator.DoesNotExist:
|
||||
message = "Aggregator does not exist"
|
||||
context = {
|
||||
"message": message,
|
||||
"class": "danger",
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
else:
|
||||
aggregators = Aggregator.objects.filter(user=self.request.user)
|
||||
|
||||
for agg in aggregators:
|
||||
agg.fetch_accounts = True
|
||||
agg.save()
|
||||
|
||||
context = {"class": "success", "message": "Fetch requested"}
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
class ReqsList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
||||
|
@ -52,7 +63,17 @@ class ReqsList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
|||
|
||||
def get_context_data(self):
|
||||
context = super().get_context_data()
|
||||
context["pk"] = self.kwargs.get("pk")
|
||||
pk = self.kwargs.get("pk")
|
||||
context["pk"] = pk
|
||||
self.extra_buttons = [
|
||||
{
|
||||
"url": reverse("bank_fetch", kwargs={"pk": pk}),
|
||||
"action": "refresh",
|
||||
"method": "get",
|
||||
"label": "Fetch account details",
|
||||
"icon": "fa-solid fa-refresh",
|
||||
},
|
||||
]
|
||||
return context
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
|
@ -61,18 +82,16 @@ class ReqsList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
|||
aggregator = Aggregator.get_by_id(pk, self.request.user)
|
||||
|
||||
except Aggregator.DoesNotExist:
|
||||
message = "Aggregator does not exist"
|
||||
message_class = "danger"
|
||||
context = {
|
||||
"message": message,
|
||||
"message_class": message_class,
|
||||
"window_content": self.window_content,
|
||||
"message": "Aggregator does not exist",
|
||||
"class": "danger",
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
self.page_title = (
|
||||
f"Requisitions for {aggregator.name} ({aggregator.get_service_display()})"
|
||||
)
|
||||
self.page_subtitle = f"Stored account details: {len(aggregator.currencies)}"
|
||||
|
||||
run = synchronize_async_helper(NordigenClient(aggregator))
|
||||
reqs = synchronize_async_helper(run.get_requisitions())
|
||||
|
@ -100,12 +119,9 @@ class AggregatorCountriesList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
|||
aggregator = Aggregator.get_by_id(pk, self.request.user)
|
||||
|
||||
except Aggregator.DoesNotExist:
|
||||
message = "Aggregator does not exist"
|
||||
message_class = "danger"
|
||||
context = {
|
||||
"message": message,
|
||||
"message_class": message_class,
|
||||
"window_content": self.window_content,
|
||||
"message": "Aggregator does not exist",
|
||||
"class": "danger",
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
@ -141,12 +157,9 @@ class AggregatorCountryBanksList(LoginRequiredMixin, OTPRequiredMixin, ObjectLis
|
|||
aggregator = Aggregator.get_by_id(pk, self.request.user)
|
||||
|
||||
except Aggregator.DoesNotExist:
|
||||
message = "Aggregator does not exist"
|
||||
message_class = "danger"
|
||||
context = {
|
||||
"message": message,
|
||||
"message_class": message_class,
|
||||
"window_content": self.window_content,
|
||||
"message": "Aggregator does not exist",
|
||||
"class": "danger",
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
@ -167,12 +180,9 @@ class AggregatorLinkBank(LoginRequiredMixin, OTPRequiredMixin, View):
|
|||
aggregator = Aggregator.get_by_id(pk, self.request.user)
|
||||
|
||||
except Aggregator.DoesNotExist:
|
||||
message = "Aggregator does not exist"
|
||||
message_class = "danger"
|
||||
context = {
|
||||
"message": message,
|
||||
"message_class": message_class,
|
||||
"window_content": self.window_content,
|
||||
"message": "Aggregator does not exist",
|
||||
"class": "danger",
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
run = synchronize_async_helper(NordigenClient(aggregator))
|
||||
|
@ -192,12 +202,9 @@ class ReqDelete(LoginRequiredMixin, OTPRequiredMixin, View):
|
|||
aggregator = Aggregator.get_by_id(pk, self.request.user)
|
||||
|
||||
except Aggregator.DoesNotExist:
|
||||
message = "Aggregator does not exist"
|
||||
message_class = "danger"
|
||||
context = {
|
||||
"message": message,
|
||||
"message_class": message_class,
|
||||
"window_content": self.window_content,
|
||||
"message": "Aggregator does not exist",
|
||||
"class": "danger",
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
run = synchronize_async_helper(NordigenClient(aggregator))
|
||||
|
@ -219,12 +226,9 @@ class ReqInfo(LoginRequiredMixin, OTPRequiredMixin, ObjectRead):
|
|||
aggregator = Aggregator.get_by_id(pk, self.request.user)
|
||||
|
||||
except Aggregator.DoesNotExist:
|
||||
message = "Aggregator does not exist"
|
||||
message_class = "danger"
|
||||
context = {
|
||||
"message": message,
|
||||
"message_class": message_class,
|
||||
"window_content": self.window_content,
|
||||
"message": "Aggregator does not exist",
|
||||
"class": "danger",
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
run = synchronize_async_helper(NordigenClient(aggregator))
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from mixins.views import ObjectList, ObjectRead
|
||||
from two_factor.views.mixins import OTPRequiredMixin
|
||||
|
||||
from core.models import Aggregator
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
|
||||
|
||||
class BanksCurrencies(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
||||
"""
|
||||
Get a list of configured currencies from the banks we use.
|
||||
"""
|
||||
|
||||
list_template = "partials/banks-currencies-list.html"
|
||||
page_title = "Bank Currencies"
|
||||
|
||||
context_object_name_singular = "currency"
|
||||
context_object_name = "currencies"
|
||||
|
||||
list_url_name = "currencies"
|
||||
list_url_args = ["type"]
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
aggregators = Aggregator.objects.filter(user=self.request.user, enabled=True)
|
||||
|
||||
account_info = {}
|
||||
for agg in aggregators:
|
||||
for bank, accounts in agg.account_info.items():
|
||||
if bank not in account_info:
|
||||
account_info[bank] = []
|
||||
for account in accounts:
|
||||
account_info[bank].append(account)
|
||||
|
||||
return account_info
|
||||
|
||||
|
||||
class BankCurrencyDetails(LoginRequiredMixin, OTPRequiredMixin, ObjectRead):
|
||||
"""
|
||||
Get the bank details for the selected currency.
|
||||
"""
|
|
@ -0,0 +1,18 @@
|
|||
import asyncio
|
||||
|
||||
|
||||
def synchronize_async_helper(to_await):
|
||||
async_response = []
|
||||
|
||||
async def run_and_capture_result():
|
||||
r = await to_await
|
||||
async_response.append(r)
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
coroutine = run_and_capture_result()
|
||||
loop.run_until_complete(coroutine)
|
||||
return async_response[0]
|
Loading…
Reference in New Issue