Implement viewing bank balances

This commit is contained in:
Mark Veidemanis 2023-03-09 18:09:29 +00:00
parent 1ee3d04ea6
commit 35ffa036ae
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
7 changed files with 152 additions and 16 deletions

View File

@ -105,4 +105,10 @@ urlpatterns = [
banks.BanksCurrencies.as_view(), banks.BanksCurrencies.as_view(),
name="currencies", name="currencies",
), ),
# Bank balances
path(
"banks/<str:type>/balances/",
banks.BanksBalances.as_view(),
name="balances",
),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -3,7 +3,6 @@ from abc import ABC
class AggregatorClient(ABC): class AggregatorClient(ABC):
def store_account_info(self, account_infos): def store_account_info(self, account_infos):
print("STORE ACCOUNT INFO CALLED")
# account_infos = { # account_infos = {
# bank: accounts # bank: accounts
# for bank, accounts in account_info.items() # for bank, accounts in account_info.items()
@ -41,6 +40,3 @@ class AggregatorClient(ABC):
self.instance.currencies = currencies self.instance.currencies = currencies
self.instance.save() self.instance.save()
print("CURRENCIES", self.instance.currencies)
print("ACCOUNT INFO", self.instance.account_info)

View File

@ -61,7 +61,7 @@ class NordigenClient(BaseClient, AggregatorClient):
""" """
# This function is a stub. # This function is a stub.
return ["GB", "SE", "BG", "UA"] return ["GB", "SE", "BG"]
async def get_banks(self, country): async def get_banks(self, country):
""" """
@ -129,7 +129,6 @@ class NordigenClient(BaseClient, AggregatorClient):
path = f"accounts/{account_id}/details" path = f"accounts/{account_id}/details"
response = await self.call(path, schema="AccountDetails") response = await self.call(path, schema="AccountDetails")
print("RESPONSE", response)
if "account" not in response: if "account" not in response:
return False return False
parsed = response["account"] parsed = response["account"]
@ -151,9 +150,7 @@ class NordigenClient(BaseClient, AggregatorClient):
async def get_all_account_info(self, requisition=None, store=False): async def get_all_account_info(self, requisition=None, store=False):
to_return = {} to_return = {}
if not requisition: if not requisition:
print("NOT REQUISITION")
requisitions = await self.get_requisitions() requisitions = await self.get_requisitions()
print("GOT REQS", requisitions)
else: else:
requisitions = [await self.get_requisition(requisition)] requisitions = [await self.get_requisition(requisition)]
@ -169,6 +166,78 @@ class NordigenClient(BaseClient, AggregatorClient):
to_return[req["institution_id"]] = [account_info] to_return[req["institution_id"]] = [account_info]
if store: if store:
print("TO STORE IS TRUE") if requisition is not None:
raise Exception("Cannot store partial data")
self.store_account_info(to_return) self.store_account_info(to_return)
return to_return return to_return
async def get_balance(self, account_id):
"""
Get the balance and currency of an account.
:param account_id: the account ID
:return: tuple of (currency, amount)
:rtype: tuple
"""
path = f"accounts/{account_id}/balances"
response = await self.call(path, schema="AccountBalance")
total = 0
currency = None
if "balances" not in response:
return (False, False)
for entry in response["balances"]:
if currency:
if not currency == entry["balanceAmount"]["currency"]:
return (False, False)
total += float(entry["balanceAmount"]["amount"])
currency = entry["balanceAmount"]["currency"]
return (currency, total)
async def get_all_balances(self):
"""
Get all balances.
Keyed by bank.
"""
if self.instance.account_info is None:
await self.get_all_account_info(store=True)
account_balances = {}
for bank, accounts in self.instance.account_info.items():
if bank not in account_balances:
account_balances[bank] = []
for account in accounts:
account_id = account["account_id"]
currency, amount = await self.get_balance(account_id)
account_balances[bank].append(
{
"currency": currency,
"balance": amount,
"account_id": account_id,
}
)
return account_balances
async def get_total_map(self):
"""
Return a dictionary keyed by currencies with the amounts as values.
:return: dict keyed by currency, values are amounts
:rtype: dict
"""
if self.instance.account_info is None:
await self.get_all_account_info(store=True)
totals = {}
for bank, accounts in self.instance.account_info.items():
for account in accounts:
account_id = account["account_id"]
currency, amount = await self.get_balance(account_id)
if not amount:
continue
if not currency:
continue
if currency in totals:
totals[currency] += amount
else:
totals[currency] = amount
return totals

View File

@ -18,9 +18,7 @@ async def job():
for aggregator in aggregators: for aggregator in aggregators:
if aggregator.service == "nordigen": if aggregator.service == "nordigen":
instance = await NordigenClient(aggregator) instance = await NordigenClient(aggregator)
print("RUNNING GET ALL ACCOUNT INFO")
await instance.get_all_account_info(store=True) await instance.get_all_account_info(store=True)
print("FINISHED RUNNING")
else: else:
raise NotImplementedError(f"No such client library: {aggregator.service}") raise NotImplementedError(f"No such client library: {aggregator.service}")
aggregator.fetch_accounts = False aggregator.fetch_accounts = False

View File

@ -231,7 +231,7 @@
<a class="navbar-item" href="{% url 'currencies' type='page' %}"> <a class="navbar-item" href="{% url 'currencies' type='page' %}">
Currencies Currencies
</a> </a>
<a class="navbar-item" href="#"> <a class="navbar-item" href="{% url 'balances' type='page' %}">
Balances Balances
</a> </a>
</div> </div>

View File

@ -0,0 +1,42 @@
{% load cache %}
{% load cachalot cache %}
{% load nsep %}
{% get_last_invalidation 'core.Aggregator' as last %}
{% include 'mixins/partials/notify.html' %}
{# cache 600 objects_banks_balances 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>balance</th>
<th>id</th>
</thead>
{% for account in accounts %}
<tr>
<td>{{ account.currency }}</td>
<td>{{ account.balance }}</td>
<td>
<a
class="has-text-grey"
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ account.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 #}

View File

@ -1,16 +1,18 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from mixins.views import ObjectList, ObjectRead from mixins.views import ObjectList
from two_factor.views.mixins import OTPRequiredMixin from two_factor.views.mixins import OTPRequiredMixin
from core.clients.aggregators.nordigen import NordigenClient
from core.models import Aggregator from core.models import Aggregator
from core.util import logs from core.util import logs
from core.views.helpers import synchronize_async_helper
log = logs.get_logger(__name__) log = logs.get_logger(__name__)
class BanksCurrencies(LoginRequiredMixin, OTPRequiredMixin, ObjectList): class BanksCurrencies(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
""" """
Get a list of configured currencies from the banks we use. Get a list of bank accounts with their details.
""" """
list_template = "partials/banks-currencies-list.html" list_template = "partials/banks-currencies-list.html"
@ -36,7 +38,30 @@ class BanksCurrencies(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
return account_info return account_info
class BankCurrencyDetails(LoginRequiredMixin, OTPRequiredMixin, ObjectRead): class BanksBalances(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
""" """
Get the bank details for the selected currency. Get the bank balances.
""" """
list_template = "partials/banks-balances-list.html"
page_title = "Bank Balances"
context_object_name_singular = "balance"
context_object_name = "balances"
list_url_name = "balances"
list_url_args = ["type"]
def get_queryset(self, **kwargs):
aggregators = Aggregator.objects.filter(user=self.request.user, enabled=True)
account_balances = {}
for aggregator in aggregators:
run = synchronize_async_helper(NordigenClient(aggregator))
balance_map = synchronize_async_helper(run.get_all_balances())
for k, v in balance_map.items():
if k not in account_balances:
account_balances[k] = []
for item in v:
account_balances[k].append(item)
return account_balances