diff --git a/app/urls.py b/app/urls.py index b9c452c..a4ddc3d 100644 --- a/app/urls.py +++ b/app/urls.py @@ -105,4 +105,10 @@ urlpatterns = [ banks.BanksCurrencies.as_view(), name="currencies", ), + # Bank balances + path( + "banks//balances/", + banks.BanksBalances.as_view(), + name="balances", + ), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/core/clients/aggregator.py b/core/clients/aggregator.py index d12a308..082aa31 100644 --- a/core/clients/aggregator.py +++ b/core/clients/aggregator.py @@ -3,7 +3,6 @@ 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() @@ -41,6 +40,3 @@ class AggregatorClient(ABC): self.instance.currencies = currencies self.instance.save() - - print("CURRENCIES", self.instance.currencies) - print("ACCOUNT INFO", self.instance.account_info) diff --git a/core/clients/aggregators/nordigen.py b/core/clients/aggregators/nordigen.py index 87f3e21..71d325d 100644 --- a/core/clients/aggregators/nordigen.py +++ b/core/clients/aggregators/nordigen.py @@ -61,7 +61,7 @@ class NordigenClient(BaseClient, AggregatorClient): """ # This function is a stub. - return ["GB", "SE", "BG", "UA"] + return ["GB", "SE", "BG"] async def get_banks(self, country): """ @@ -129,7 +129,6 @@ class NordigenClient(BaseClient, AggregatorClient): 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"] @@ -151,9 +150,7 @@ class NordigenClient(BaseClient, AggregatorClient): async def get_all_account_info(self, requisition=None, store=False): to_return = {} if not requisition: - print("NOT REQUISITION") requisitions = await self.get_requisitions() - print("GOT REQS", requisitions) else: requisitions = [await self.get_requisition(requisition)] @@ -169,6 +166,78 @@ class NordigenClient(BaseClient, AggregatorClient): to_return[req["institution_id"]] = [account_info] if store: - print("TO STORE IS TRUE") + if requisition is not None: + raise Exception("Cannot store partial data") self.store_account_info(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 diff --git a/core/management/commands/scheduling.py b/core/management/commands/scheduling.py index 87ebbc5..2b12232 100644 --- a/core/management/commands/scheduling.py +++ b/core/management/commands/scheduling.py @@ -18,9 +18,7 @@ async def job(): 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 diff --git a/core/templates/base.html b/core/templates/base.html index afd19dd..9b9ba7f 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -231,7 +231,7 @@ Currencies - + Balances diff --git a/core/templates/partials/banks-balances-list.html b/core/templates/partials/banks-balances-list.html new file mode 100644 index 0000000..9d5fc77 --- /dev/null +++ b/core/templates/partials/banks-balances-list.html @@ -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 %} +

{{ bank }}

+ + + + + + + + {% for account in accounts %} + + + + + + {% endfor %} + +
currencybalanceid
{{ account.currency }}{{ account.balance }} + + + + + +
+{% endfor %} +{# endcache #} \ No newline at end of file diff --git a/core/views/banks.py b/core/views/banks.py index 1eed5a2..3d409f9 100644 --- a/core/views/banks.py +++ b/core/views/banks.py @@ -1,16 +1,18 @@ 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 core.clients.aggregators.nordigen import NordigenClient from core.models import Aggregator from core.util import logs +from core.views.helpers import synchronize_async_helper log = logs.get_logger(__name__) 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" @@ -36,7 +38,30 @@ class BanksCurrencies(LoginRequiredMixin, OTPRequiredMixin, ObjectList): 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