Implement account handling for Nordigen
This commit is contained in:
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user