Commit 6e7d12cb authored by sinopsysHK's avatar sinopsysHK Committed by Laurent Bachelier

[hsbchk] create new HSBC boobank module dedicated to HK accounts

Despite looking close to French web site, it Hong Kong web site is
completely different and need its own support.
So far login, getting accounts and account history are working.
There is no such "coming" entries in Hong Kong as all entries are
recorded alnost in real time even for credit cards which are deferred
toward parent account but real time toward own account.
Profile might be added later.

There is no support yet for investments, loans and deposits as I have no
mean to test.

There is an enhancement that could be considered: enrich entries from
FPS (Fast Payment Service) additional details. Not yet supported
parent 259f8eb6
Pipeline #2755 failed with stages
in 1 minute and 14 seconds
from .module import HSBCHKModule
__all__ = ['HSBCHKModule']
# -*- coding: utf-8 -*-
# Copyright(C) 2012-2013 Romain Bignon
#
# This file is part of a weboob module.
#
# This weboob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This weboob module is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from datetime import timedelta, date, datetime
from dateutil import parser
from weboob.exceptions import NoAccountsException
from weboob.browser import PagesBrowser, URL, need_login, StatesMixin
from weboob.browser.selenium import SubSeleniumMixin
from weboob.browser.exceptions import LoggedOut, ClientError
from .pages.account_pages import (
OtherPage, JsonAccSum, JsonAccHist
)
from .sbrowser import LoginBrowser
__all__ = ['HSBCHK']
class HSBCHK(StatesMixin, SubSeleniumMixin, PagesBrowser):
BASEURL = 'https://www.services.online-banking.hsbc.com.hk/gpib/group/gpib/cmn/layouts/default.html?uid=dashboard'
STATE_DURATION = 15
app_gone = False
acc_summary = URL(r'https://www.services.online-banking.hsbc.com.hk/gpib/channel/proxy/accountDataSvc/rtrvAcctSumm', JsonAccSum)
acc_history = URL('https://www.services.online-banking.hsbc.com.hk/gpib/channel/proxy/accountDataSvc/rtrvTxnSumm', JsonAccHist)
# catch-all
other_page = URL(
r' https://www.services.online-banking.hsbc.com.hk/gpib/systemErrorRedirect.html.*',
OtherPage)
__states__ = ('auth_token', 'logged', 'selenium_state')
def __init__(self, username, password, secret, *args, **kwargs):
super(HSBCHK, self).__init__(*args, **kwargs)
# accounts index changes at each session
self.accounts_dict_idx = None
self.username = username
self.password = password
self.secret = secret
self.logged = False
self.auth_token = None
def create_selenium_browser(self):
dirname = self.responses_dirname
if dirname:
dirname += '/selenium'
return LoginBrowser(
self.username,
self.password,
self.secret,
logger=self.logger,
responses_dirname=dirname,
proxy=self.PROXIES
)
def load_selenium_session(self, selenium):
super(HSBCHK, self).load_selenium_session(selenium)
self.location(
selenium.url,
referrer="https://www.security.online-banking.hsbc.com.hk/gsa/SaaS30Resource/"
)
def load_state(self, state):
if ('expire' in state and parser.parse(state['expire']) > datetime.now()) or state.get('auth_token'):
return super(HSBCHK, self).load_state(state)
def open(self, *args, **kwargs):
try:
return super(HSBCHK, self).open(*args, **kwargs)
except ClientError as e:
if e.response.status_code == 401:
self.auth_token = None
self.logged = False
self.session.cookies.clear()
raise LoggedOut()
if e.response.status_code == 409:
raise NoAccountsException()
raise
def do_login(self):
self.auth_token = None
self.logger.debug("currrent state is Logged:%s", self.logged)
super(HSBCHK, self).do_login()
self.auth_token = self.session.cookies.get('SYNC_TOKEN')
self.logged = True
@need_login
def iter_accounts(self):
# on new session initialize accounts dict
if not self.accounts_dict_idx:
self.accounts_dict_idx = dict()
self.update_header()
jq = {"accountSummaryFilter":{"txnTypCdes":[],"entityCdes":[{"ctryCde":"HK","grpMmbr":"HBAP"}]}}
for a in self.acc_summary.go(json = jq).iter_accounts():
self.accounts_dict_idx[a.id] = a
yield a
@need_login
def get_history(self, account, coming=False, retry_li=True):
if not self.accounts_dict_idx:
self.iter_accounts()
self.update_header()
today = date.today()
fromdate = today - timedelta(100)
jq = {
"retreiveTxnSummaryFilter": {
"txnDatRnge": {
"fromDate": fromdate.isoformat(),
"toDate": today.isoformat()
},
"numOfRec": -1,
"txnAmtRnge": None,
"txnHistType": None # "U"
},
"acctIdr": {
"acctIndex": self.accounts_dict_idx[account.id]._idx,
"entProdTypCde": account._entProdTypCde,
"entProdCatCde": account._entProdCatCde
},
"pagingInfo": {
"startDetail": None,
"pagingDirectionCode": "PD"
},
"extensions": None
}
try:
self.acc_history.go(json = jq)
except NoAccountsException:
return []
return self.page.iter_history()
def update_header(self):
self.session.headers.update({
"Origin":"https://www.services.online-banking.hsbc.com.hk",
"Referer":"https://www.services.online-banking.hsbc.com.hk/gpib/group/gpib/cmn/layouts/default.html?uid=dashboard",
"Content-type":"application/json",
"X-HDR-Synchronizer-Token": self.session.cookies.get('SYNC_TOKEN')
})
# -*- coding: utf-8 -*-
# Copyright(C) 2012-2013 Romain Bignon
#
# This file is part of a weboob module.
#
# This weboob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This weboob module is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
#from weboob.capabilities.bank import CapBankWealth, AccountNotFound
from weboob.capabilities.bank import CapBank, AccountNotFound
from weboob.capabilities.base import find_object
from weboob.tools.backend import Module, BackendConfig
from weboob.tools.value import ValueBackendPassword
from .browser import HSBCHK
__all__ = ['HSBCHKModule']
class HSBCHKModule(Module, CapBank):
NAME = 'hsbchk'
MAINTAINER = u'sinopsysHK'
EMAIL = 'sinofwd@gmail.com'
VERSION = '1.6'
LICENSE = 'LGPLv3+'
DESCRIPTION = 'HSBC Hong Kong'
CONFIG = BackendConfig(ValueBackendPassword('login', label='User identifier', masked=False),
ValueBackendPassword('password', label='Password'),
ValueBackendPassword('secret', label=u'Memorable answer'))
BROWSER = HSBCHK
def create_default_browser(self):
return self.create_browser(
self.config['login'].get(),
self.config['password'].get(),
self.config['secret'].get()
)
def iter_accounts(self):
for account in self.browser.iter_accounts():
yield account
def get_account(self, _id):
return find_object(self.browser.iter_accounts(), id=_id, error=AccountNotFound)
def iter_history(self, account):
for tr in self.browser.get_history(account):
yield tr
def iter_investment(self, account):
raise NotImplemented
def iter_coming(self, account):
# No coming entries on HSBC HK
return []
# -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Julien Veyssier
#
# This file is part of a weboob module.
#
# This weboob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This weboob module is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
import re
from decimal import Decimal
import requests
import json
from weboob.browser.elements import DictElement, ItemElement, method
from weboob.browser.filters.json import Dict
from weboob.browser.filters.standard import (
CleanDecimal, CleanText, Date
)
from weboob.browser.pages import HTMLPage, LoggedPage, pagination, JsonPage
from weboob.capabilities.bank import Account
from weboob.exceptions import ActionNeeded, BrowserIncorrectPassword
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
class Transaction(FrenchTransaction):
PATTERNS = [
(re.compile(r'^PAYMENT - THANK YOU'), FrenchTransaction.TYPE_CARD_SUMMARY),
(re.compile(r'^CREDIT CARD PAYMENT (?P<text>.*)'), FrenchTransaction.TYPE_CARD_SUMMARY),
(re.compile(r'^CREDIT INTEREST'), FrenchTransaction.TYPE_BANK),
(re.compile(r'DEBIT INTEREST'), FrenchTransaction.TYPE_BANK),
(re.compile(r'^UNAUTHORIZED OD CHARGE'), FrenchTransaction.TYPE_BANK),
(re.compile(r'^ATM WITHDRAWAL\ *\((?P<dd>\d{2})(?P<mmm>\w{3})(?P<yy>\d{2})\)'), FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile(r'^POS CUP\ *\((?P<dd>\d{2})(?P<mmm>\w{3})(?P<yy>\d{2})\)\ *(?P<text>.*)'), FrenchTransaction.TYPE_CARD),
(re.compile(r'^EPS\d*\ *\((?P<dd>\d{2})(?P<mmm>\w{3})(?P<yy>\d{2})\)\ *(?P<text>.*)'), FrenchTransaction.TYPE_CARD),
(re.compile(r'^CR TO (?P<text>.*)\((?P<dd>\d{2})(?P<mmm>\w{3})(?P<yy>\d{2})\)'), FrenchTransaction.TYPE_TRANSFER),
(re.compile(r'^FROM (?P<text>.*)\((?P<dd>\d{2})(?P<mmm>\w{3})(?P<yy>\d{2})\)'), FrenchTransaction.TYPE_TRANSFER),
]
class JsonBasePage(JsonPage):
def on_load(self):
coderet = Dict('responseInfo/reasons/0/code')(self.doc)
conditions = (
coderet == '000',
)
assert any(conditions), 'Error %s is not handled yet' % coderet
class JsonAccSum(LoggedPage, JsonBasePage):
PRODUCT_TO_TYPE = {
u"SAV": Account.TYPE_SAVINGS,
u"CUR": Account.TYPE_CHECKING,
u"TD": Account.TYPE_DEPOSIT,
u"INV": Account.TYPE_MARKET,
u"CC": Account.TYPE_CARD,
}
LABEL = {
u"SAV": "Savings",
u"CUR": "Current",
u"TD": "Time deposit",
u"INV": "Investment",
u"CC": "Credit card",
}
def get_acc_dict(self, json):
acc_dict = {}
if json.get('hasAcctDetails'):
acc_dict = {
'bank_name': 'HSBC HK',
'id': '%s-%s-%s' % (
json.get('displyID'),
json.get('prodCatCde'),
json.get('ldgrBal').get('ccy')
),
'_idx': json.get('acctIndex'),
'_entProdCatCde': json.get('entProdCatCde'),
'_entProdTypCde': json.get('entProdTypCde'),
'number': json.get('displyID'),
'type': self.PRODUCT_TO_TYPE.get(json.get('prodCatCde')) or Account.TYPE_UNKNOWN,
'label': '%s %s' % (
self.LABEL.get(json.get('prodCatCde')),
json.get('ldgrBal').get('ccy')
),
'currency': json.get('ldgrBal').get('ccy'),
'balance': Decimal(json.get('ldgrBal').get('amt')),
}
else:
acc_dict = {
'bank_name': 'HSBC HK',
'id': '%s-%s' % (
json.get('displyID'),
json.get('prodCatCde')
),
'_idx': json.get('acctIndex'),
'number': json.get('displyID'),
'type': self.PRODUCT_TO_TYPE.get(json.get('prodCatCde')) or Account.TYPE_UNKNOWN,
'label': '%s' % (
self.LABEL.get(json.get('prodCatCde'))
)
}
return acc_dict
def iter_accounts(self):
for country in self.get('countriesAccountList'):
for acc in country.get('acctLiteWrapper'):
acc_prod = acc.get('prodCatCde')
if acc_prod == "MST":
for subacc in acc.get('subAcctInfo'):
res = Account.from_dict(self.get_acc_dict(subacc))
if subacc.get('hasAcctDetails'):
yield res
else:
self.logger.debug("skip account with no history: %s", res)
elif acc_prod == "CC":
self.logger.debug("acc: %s", str(acc))
res = Account.from_dict(self.get_acc_dict(acc))
if acc.get('hasAcctDetails'):
yield res
else:
self.logger.debug("skip account with no history: %s", res)
else:
self.logger.error("Unknown account product code [%s]", acc_prod)
class JsonAccHist(LoggedPage, JsonBasePage):
@pagination
@method
class iter_history(DictElement):
item_xpath = "txnSumm"
def next_page(self):
self.logger.debug(
"paging (%s): %s",
Dict('responsePagingInfo/moreRecords', default='N')(self.page.doc),
self.page.doc.get('responsePagingInfo'))
if Dict('responsePagingInfo/moreRecords', default='N')(self.page.doc) == 'Y':
self.logger.info("more values are available")
"""
prev_req = self.page.response.request
jq = json.loads(prev_req.body)
jq['pagingInfo']['startDetail']=Dict('responsePagingInfo/endDetail')(self.page.doc)
return requests.Request(
self.page.url,
headers = prev_req.headers,
json = jq
)
"""
return
class item(ItemElement):
klass = Transaction
obj_rdate = Date(Dict('txnDate'))
obj_date = Date(Dict('txnPostDate'))
obj_amount = CleanDecimal(Dict('txnAmt/amt'))
def obj_raw(self):
return Transaction.Raw(Dict('txnDetail/0'))(self)
def obj_type(self):
for pattern, type in Transaction.PATTERNS:
if pattern.match(Dict('txnDetail/0')(self)):
return type
if Dict('txnHistType', default=None)(self) in ['U', 'B']:
return Transaction.TYPE_CARD
return Transaction.TYPE_TRANSFER
class AppGonePage(HTMLPage):
def on_load(self):
self.browser.app_gone = True
self.logger.info('Application has gone. Relogging...')
self.browser.do_logout()
self.browser.do_login()
class OtherPage(HTMLPage):
ERROR_CLASSES = [
('Votre contrat est suspendu', ActionNeeded),
("Vos données d'identification (identifiant - code secret) sont incorrectes", BrowserIncorrectPassword),
('Erreur : Votre contrat est clôturé.', ActionNeeded),
]
def on_load(self):
for msg, exc in self.ERROR_CLASSES:
for tag in self.doc.xpath('//p[@class="debit"]//strong[text()[contains(.,$msg)]]', msg=msg):
raise exc(CleanText('.')(tag))
# -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Julien Veyssier
#
# This file is part of a weboob module.
#
# This weboob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This weboob module is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from weboob.browser.filters.standard import (
CleanText
)
from weboob.browser.selenium import (
SeleniumPage, VisibleXPath
)
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable
from selenium.webdriver.common.by import By
class LoginPage(SeleniumPage):
is_here = VisibleXPath('//h2[text()[contains(.,"Log on to Internet Banking")]]')
@property
def logged(self):
if self.doc.xpath('//p[contains(text(), "You are now being redirected to your Personal Internet Banking.")]'):
return True
return False
def on_load(self):
for message in self.doc.xpath('//div[contains(@class, "alertBox")]'):
error_msg = CleanText('.')(message)
if any(msg in error_msg for msg in ['The username you entered doesn\'t match our records. Please try again.',
'Please enter your memorable answer and password.',
'The information you entered does not match our records. Please try again.',
'mot de passe invalide']):
raise BrowserIncorrectPassword(error_msg)
else:
raise BrowserUnavailable(error_msg)
def get_error(self):
for message in self.doc.xpath('//div[contains(@data-dojo-type, "hsbcwidget/alertBox")]'):
error_msg = CleanText('.')(message)
if any(msg in error_msg for msg in ['The username you entered doesn\'t match our records. Please try again.',
'Please enter a valid Username.',
'mot de passe invalide']):
raise BrowserIncorrectPassword(error_msg)
else:
raise BrowserUnavailable(error_msg)
return
def login(self, login):
self.driver.find_element_by_name("userid").send_keys(login)
self.driver.find_element_by_class_name("submit_input").click()
def get_no_secure_key(self):
self.driver.find_element_by_xpath('//a[span[contains(text(), "Without Security Device")]]').click()
def login_w_secure(self, password, secret):
self.driver.find_element_by_name("memorableAnswer").send_keys(secret)
if len(password) < 8:
raise BrowserIncorrectPassword('The password must be at least %d characters' % 8)
elif len(password) > 8:
# HSBC only use 6 first and last two from the password
password = password[:6] + password[-2:]
elts = self.driver.find_elements(By.XPATH, "//input[@type='password' and contains(@id,'pass')]")
for elt in elts:
if elt.get_attribute('disabled') is None and elt.get_attribute('class') == "smallestInput active":
elt.send_keys(password[int(elt.get_attribute('id')[-1]) - 1])
self.driver.find_element_by_xpath("//input[@class='submit_input']").click()
# -*- coding: utf-8 -*-
# Copyright(C) 2012-2013 Romain Bignon
#
# This file is part of a weboob module.
#
# This weboob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This weboob module is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
import os
try:
from selenium import webdriver
except ImportError:
raise ImportError('Please install python-selenium')
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.common.proxy import Proxy, ProxyType
from selenium.common.exceptions import (
TimeoutException
)
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable, BrowserHTTPError
from weboob.browser.selenium import (SeleniumBrowser, DirFirefoxProfile, VisibleXPath)
from weboob.browser import URL
from .pages.login import (
LoginPage
)
class LoginBrowser(SeleniumBrowser):
BASEURL = 'https://www.hsbc.com.hk/'
#DRIVER = webdriver.Remote
app_gone = False
preconnection = URL(r'https://www.ebanking.hsbc.com.hk/1/2/logon?LANGTAG=en&COUNTRYTAG=US', LoginPage)
login = URL(r'https://www.security.online-banking.hsbc.com.hk/gsa/SaaS30Resource/*', LoginPage)
def __init__(self, username, password, secret, *args, **kwargs):
super(LoginBrowser, self).__init__(*args, **kwargs)
self.username = username
self.password = password
self.secret = secret
self.web_space = None
self.home_url = None
def _build_capabilities(self):
capa = super(LoginBrowser, self)._build_capabilities()
capa['marionette'] = True
return capa
def _setup_driver(self):
proxy = Proxy()
proxy.proxy_type = ProxyType.DIRECT
if 'http' in self.proxy:
proxy.http_proxy = self.proxy['http']
if 'https' in self.proxy:
proxy.ssl_proxy = self.proxy['https']
capa = self._build_capabilities()
proxy.add_to_capabilities(capa)
options = self._build_options()
# TODO some browsers don't need headless
# TODO handle different proxy setting?
options.set_headless(self.HEADLESS)
if self.DRIVER is webdriver.Firefox:
if self.responses_dirname and not os.path.isdir(self.responses_dirname):
os.makedirs(self.responses_dirname)
options.profile = DirFirefoxProfile(self.responses_dirname)
if self.responses_dirname:
capa['profile'] = self.responses_dirname
self.driver = self.DRIVER(options=options, capabilities=capa)
elif self.DRIVER is webdriver.Chrome:
self.driver = self.DRIVER(options=options, desired_capabilities=capa)
elif self.DRIVER is webdriver.PhantomJS:
if self.responses_dirname:
if not os.path.isdir(self.responses_dirname):
os.makedirs(self.responses_dirname)
log_path = os.path.join(self.responses_dirname, 'selenium.log')
else:
log_path = NamedTemporaryFile(prefix='weboob_selenium_', suffix='.log', delete=False).name
self.driver = self.DRIVER(desired_capabilities=capa, service_log_path=log_path)
elif self.DRIVER is webdriver.Remote:
# self.HEADLESS = False
# for debugging purpose
self.driver = webdriver.Remote(
command_executor='http://<selenium host>:<selenium port>/wd/hub',
desired_capabilities=DesiredCapabilities.FIREFOX)
else:
raise NotImplementedError()
if self.WINDOW_SIZE:
self.driver.set_window_size(*self.WINDOW_SIZE)
def load_state(self, state):
return
def do_login(self):
self.logger.debug("start do_login")
self.app_gone = False
self.preconnection.go()
try:
self.wait_until(VisibleXPath('//h2[text()[contains(.,"Log on to Internet Banking")]]'), timeout=20)
self.page.login(self.username)
self.wait_until_is_here(self.login, 10)
error = self.page.get_error()
if error:
raise BrowserIncorrectPassword(error)
self.page.get_no_secure_key()
self.wait_until_is_here(self.login, 10)
error = self.page.get_error()
if error:
raise BrowserHTTPError(error)
self.page.login_w_secure(self.password, self.secret)
if self.login.is_here():
error = self.page.get_error()
if error:
raise BrowserIncorrectPassword(error)
WebDriverWait(self.driver, 20).until(EC.title_contains("My banking"))
except TimeoutException as e:
self.logger.exception("timeout while login")
raise BrowserUnavailable(e.msg)
# -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon
#
# This file is part of a weboob module.
#
# This weboob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This weboob module is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from weboob.tools.test import BackendTest
from weboob.capabilities.bank import Account
class HSBCHKTest(BackendTest):
MODULE = 'hsbchk'
def test_hsbchk(self):
l = list(self.backend.iter_accounts())
if len(l) > 0:
a = l[0]
list(self.backend.iter_history(a))
# def test_investments(self):
# life_insurance_accounts = [account for account in self.backend.iter_accounts() if account.type == Account.TYPE_LIFE_INSURANCE]
# investments = {acc.id: list(self.backend.iter_investment(acc)) for acc in life_insurance_accounts}
# for acc in life_insurance_accounts:
# invs = investments[acc.id]
# self.assertLessEquals(sum([inv.valuation for inv in invs]), acc.balance)