Implement account handling for Nordigen

This commit is contained in:
2022-03-22 21:56:35 +00:00
parent 2ca518962c
commit 7b38e487cf
5 changed files with 259 additions and 15 deletions

View File

@@ -29,7 +29,7 @@ class Sinks(util.Base):
any race conditions by relying on something that might not be there.
"""
self.fidor = sinks.fidor.Fidor()
self.nordigen = sinks.nordigen.Nordigen()
self.nordigen = sinks.nordigen.Nordigen(self)
self.truelayer = sinks.truelayer.TrueLayer(self)
# setattr(self.truelayer, "sinks", self)
@@ -62,15 +62,24 @@ class Sinks(util.Base):
:param account_infos: dict of dicts of account information
:param account_infos: dict
"""
for bank, accounts in account_infos.items():
for account in list(accounts):
if len(account["account_number"]) == 1:
account_infos[bank].remove(account)
self.log.warning(f"Potentially useless bank account: {account}")
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)
self.log.warning(f"Potentially useless bank account: {account}")
currencies = [account["currency"] for bank, accounts in account_infos.items() for account in accounts]
self.account_info = account_infos
for bank, accounts in account_infos.items():
self.account_info[bank] = []
for account in accounts:
self.account_info[bank].append(account)
# self.account_info = account_infos
self.currencies = currencies
# parsed_details =

View File

@@ -1,7 +1,10 @@
# Twisted/Klein imports
from twisted.internet.task import LoopingCall
# Other library imports
import requests
from json import dumps
from simplejson.errors import JSONDecodeError
from json import dumps, loads
# Project imports
from settings import settings
@@ -13,10 +16,32 @@ class Nordigen(util.Base):
Class to manage calls to Open Banking APIs through Nordigen.
"""
def __init__(self):
def __init__(self, sinks):
super().__init__()
self.sinks = sinks
self.token = None
self.get_access_token()
self.banks = {}
self.authed = False
# Get the banks from the config and cache them
self.get_mapped_accounts()
self.lc = LoopingCall(self.get_access_token)
self.lc.start(int(settings.Nordigen.TokenRefreshSec))
def __authed__(self):
"""
Called when we have received the access token.
"""
self.log.info("Connection authenticated.")
account_infos = self.get_all_account_info()
# Filter for added accounts since we only do that for TrueLayer
account_infos = {
bank: accounts for bank, accounts in account_infos.items() for account in accounts if account["account_id"] in self.banks
}
self.sinks.got_account_info("nordigen", account_infos)
def get_access_token(self):
"""
@@ -39,6 +64,11 @@ class Nordigen(util.Base):
if "access" in parsed:
self.token = parsed["access"]
self.log.info("Refreshed access token")
else:
self.log.error(f"Access token not in response: {parsed}")
if not self.authed:
self.__authed__()
self.authed = True
def get_institutions(self, country, filter_name=None):
"""
@@ -65,3 +95,160 @@ class Nordigen(util.Base):
new_list.append(i)
return new_list
return parsed
def create_agreement(self, institution_id):
"""Create an agreement to access an institution.
:param institution_id: ID of the institution
"""
headers = {"accept": "application/json", "Authorization": f"Bearer {self.token}"}
path = f"{settings.Nordigen.Base}/agreements/enduser"
data = {"institution_id": institution_id}
r = requests.post(path, headers=headers, data=dumps(data))
try:
parsed = r.json()
except JSONDecodeError:
self.log.error(f"Error parsing agreement response: {r.content}")
return False
return parsed
def build_link(self, institution_id):
"""Create a link to access an institution.
:param institution_id: ID of the institution
"""
headers = {"accept": "application/json", "Authorization": f"Bearer {self.token}"}
path = f"{settings.Nordigen.Base}/requisitions/"
data = {"institution_id": institution_id, "redirect": settings.Nordigen.CallbackURL}
r = requests.post(path, headers=headers, data=data)
try:
parsed = r.json()
except JSONDecodeError:
self.log.error(f"Error parsing link response: {r.content}")
return False
if "link" in parsed:
return parsed["link"]
return False
def create_auth_url(self, country, bank_name):
"""Helper to look up a bank and create a link.
:param country: country
:param bank_name: bank name string to search"""
institutions = self.get_institutions(country, filter_name=bank_name)
# We were not precise enough to have one result
if not len(institutions) == 1:
return False
institution = institutions[0]
link = self.build_link(institution["id"])
if not link:
return False
return link
def get_requisitions(self):
"""
Get a list of active accounts.
"""
headers = {"accept": "application/json", "Authorization": f"Bearer {self.token}"}
path = f"{settings.Nordigen.Base}/requisitions"
r = requests.get(path, headers=headers)
try:
parsed = r.json()
except JSONDecodeError:
self.log.error(f"Error parsing requisitions response: {r.content}")
return False
if "results" in parsed:
return parsed["results"]
else:
self.log.error(f"Results not in requisitions response: {parsed}")
return False
def get_accounts(self, requisition):
"""
Get a list of accounts for a requisition.
:param requisition: requisition ID"""
headers = {"accept": "application/json", "Authorization": f"Bearer {self.token}"}
path = f"{settings.Nordigen.Base}/requisitions/{requisition}/"
r = requests.get(path, headers=headers)
try:
parsed = r.json()
except JSONDecodeError:
self.log.error(f"Error parsing accounts response: {r.content}")
return False
if "accounts" in parsed:
return parsed["accounts"]
return False
def get_account(self, account_id):
"""
Get details of an account.
:param requisition: requisition ID"""
headers = {"accept": "application/json", "Authorization": f"Bearer {self.token}"}
path = f"{settings.Nordigen.Base}/accounts/{account_id}/details/"
r = requests.get(path, headers=headers)
try:
parsed = r.json()
except JSONDecodeError:
self.log.error(f"Error parsing account response: {r.content}")
return False
# if "accounts" in parsed:
# return parsed["accounts"]
# return False
parsed = parsed["account"]
if "bban" in parsed and parsed["currency"] == "GBP":
sort_code = parsed["bban"][0:6]
account_number = parsed["bban"][6:]
del parsed["bban"]
del parsed["iban"]
sort_code = "-".join(list(map("".join, zip(*[iter(sort_code)] * 2))))
parsed["sort_code"] = sort_code
parsed["number"] = account_number
# Let's add the account ID so we can reference it later
parsed["account_id"] = account_id
return parsed
def get_mapped_accounts(self):
existing_entry = loads(settings.Nordigen.Maps)
self.banks = existing_entry
def map_account(self, account_id):
"""
Map an account_id at a bank to an account_name.
This enables the account for fetching.
Data type: {"monzo": [account, ids, here],
"revolut": [account, ids, here]}
"""
account_data = self.get_account(account_id)
currency = account_data["currency"]
existing_entry = loads(settings.Nordigen.Maps)
if account_id in existing_entry:
return
else:
existing_entry.append(account_id)
settings.Nordigen.Maps = dumps(existing_entry)
self.banks = existing_entry
settings.write()
return currency
def get_all_account_info(self):
to_return = {}
requisitions = self.get_requisitions()
if not requisitions:
self.log.error("Could not get requisitions.")
return {}
for req in requisitions:
if not req["accounts"]:
continue
accounts = self.get_accounts(req["id"])
for account_id in accounts:
# if not account_id in self.banks:
# print("account_id", account_id, "not in self.banks!")
# continue
account_info = self.get_account(account_id)
if not account_info:
continue
if req["institution_id"] in to_return:
to_return[req["institution_id"]].append(account_info)
else:
to_return[req["institution_id"]] = [account_info]
return to_return