Commit 5cf9b644 authored by Quentin Defenouillere's avatar Quentin Defenouillere Committed by Romain Bignon

[cragr] Implemented navigation for new Cragr website

Crédit Agricole now uses an API.
This commit enables site switching for the connections of the new
website and keeps the former behavior for the connections that still use
the previous website.
This commit was tested with the backend and we observed no duplicated
account since all the account IDs correctly match the former website
behavior.
All other methods than iter_accounts() return BrowserUnavailable for
now.

Closes: 9101@zendesk
parent c2c8179b
This diff is collapsed.
# -*- coding: utf-8 -*-
# Copyright(C) 2012-2019 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from decimal import Decimal
import re
import json
from weboob.browser.pages import HTMLPage, JsonPage, LoggedPage
from weboob.exceptions import BrowserUnavailable
from weboob.capabilities import NotAvailable
from weboob.capabilities.bank import (
Account, AccountOwnerType,
)
from weboob.browser.elements import DictElement, ItemElement, method
from weboob.browser.filters.standard import (
CleanText, CleanDecimal, Currency as CleanCurrency, Format, Field, Map, Eval
)
from weboob.browser.filters.json import Dict
def float_to_decimal(f):
return Decimal(str(f))
class KeypadPage(JsonPage):
def build_password(self, password):
# Fake Virtual Keyboard: just get the positions of each digit.
key_positions = [i for i in Dict('keyLayout')(self.doc)]
return str(','.join([str(key_positions.index(i)) for i in password]))
def get_keypad_id(self):
return Dict('keypadId')(self.doc)
class LoginPage(HTMLPage):
def get_login_form(self, username, keypad_password, keypad_id):
form = self.get_form(id="loginForm")
form['j_username'] = username
form['j_password'] = keypad_password
form['keypadId'] = keypad_id
return form
class LoggedOutPage(HTMLPage):
def is_here(self):
return self.doc.xpath('//b[text()="FIN DE CONNEXION"]')
def on_load(self):
self.logger.warning('We have been logged out!')
raise BrowserUnavailable()
class SecurityPage(JsonPage):
def get_accounts_url(self):
return Dict('url')(self.doc)
class ContractsPage(LoggedPage, HTMLPage):
pass
ACCOUNT_TYPES = {
'CCHQ': Account.TYPE_CHECKING, # par
'CCOU': Account.TYPE_CHECKING, # pro
'AUTO ENTRP': Account.TYPE_CHECKING, # pro
'DEVISE USD': Account.TYPE_CHECKING,
'EKO': Account.TYPE_CHECKING,
'DAV NANTI': Account.TYPE_SAVINGS,
'LIV A': Account.TYPE_SAVINGS,
'LIV A ASS': Account.TYPE_SAVINGS,
'LDD': Account.TYPE_SAVINGS,
'PEL': Account.TYPE_SAVINGS,
'CEL': Account.TYPE_SAVINGS,
'CODEBIS': Account.TYPE_SAVINGS,
'LJMO': Account.TYPE_SAVINGS,
'CSL': Account.TYPE_SAVINGS,
'LEP': Account.TYPE_SAVINGS,
'LEF': Account.TYPE_SAVINGS,
'TIWI': Account.TYPE_SAVINGS,
'CSL LSO': Account.TYPE_SAVINGS,
'CSL CSP': Account.TYPE_SAVINGS,
'ESPE INTEG': Account.TYPE_SAVINGS,
'DAV TIGERE': Account.TYPE_SAVINGS,
'CPTEXCPRO': Account.TYPE_SAVINGS,
'CPTEXCENT': Account.TYPE_SAVINGS,
'PRET PERSO': Account.TYPE_LOAN,
'P. ENTREPR': Account.TYPE_LOAN,
'P. HABITAT': Account.TYPE_LOAN,
'PRET 0%': Account.TYPE_LOAN,
'INV PRO': Account.TYPE_LOAN,
'TRES. PRO': Account.TYPE_LOAN,
'PEA': Account.TYPE_PEA,
'PEAP': Account.TYPE_PEA,
'DAV PEA': Account.TYPE_PEA,
'CPS': Account.TYPE_MARKET,
'TITR': Account.TYPE_MARKET,
'TITR CTD': Account.TYPE_MARKET,
'PVERT VITA': Account.TYPE_PERP,
'réserves de crédit': Account.TYPE_CHECKING,
'prêts personnels': Account.TYPE_LOAN,
'crédits immobiliers': Account.TYPE_LOAN,
'épargne disponible': Account.TYPE_SAVINGS,
'épargne à terme': Account.TYPE_DEPOSIT,
'épargne boursière': Account.TYPE_MARKET,
'assurance vie et capitalisation': Account.TYPE_LIFE_INSURANCE,
'PRED': Account.TYPE_LIFE_INSURANCE,
'PREDI9 S2': Account.TYPE_LIFE_INSURANCE,
'V.AVENIR': Account.TYPE_LIFE_INSURANCE,
'FLORIA': Account.TYPE_LIFE_INSURANCE,
'ATOUT LIB': Account.TYPE_REVOLVING_CREDIT,
}
class AccountsPage(LoggedPage, JsonPage):
def build_doc(self, content):
# Store the HTML doc to count the number of spaces
self.html_doc = HTMLPage(self.browser, self.response).doc
# Transform the HTML tag containing the accounts list into a JSON
raw = re.search("syntheseController\.init\((.*)\)'>", content).group(1)
d = json.JSONDecoder()
# De-comment this line to debug the JSON accounts:
# print json.dumps(d.raw_decode(raw)[0])
return d.raw_decode(raw)[0]
def count_spaces(self):
# The total number of spaces corresponds to the number
# of available space choices plus the one we are on now:
return len(self.html_doc.xpath('//div[@class="HubAccounts-content"]/a')) + 1
def get_owner_type(self):
OWNER_TYPES = {
'PARTICULIER': AccountOwnerType.PRIVATE,
'PROFESSIONNEL': AccountOwnerType.ORGANIZATION,
'ASSOC_CA_MODERE': AccountOwnerType.ORGANIZATION,
}
return OWNER_TYPES.get(Dict('marche')(self.doc), NotAvailable)
@method
class get_main_account(ItemElement):
klass = Account
obj_id = CleanText(Dict('comptePrincipal/numeroCompte'))
obj_number = CleanText(Dict('comptePrincipal/numeroCompte'))
obj_label = CleanText(Dict('comptePrincipal/libelleProduit'))
obj_balance = Eval(float_to_decimal, Dict('comptePrincipal/solde'))
obj_currency = CleanCurrency(Dict('comptePrincipal/idDevise'))
obj__index = Dict('comptePrincipal/index')
obj__category = None # Main accounts have no category
obj__id_element_contrat = CleanText(Dict('comptePrincipal/idElementContrat'))
def obj_type(self):
_type = Map(CleanText(Dict('comptePrincipal/libelleUsuelProduit')), ACCOUNT_TYPES, Account.TYPE_UNKNOWN)(self)
if _type == Account.TYPE_UNKNOWN:
self.logger.warning('We got an untyped account: please add "%s" to ACCOUNT_TYPES.' % CleanText(Dict('comptePrincipal/libelleUsuelProduit'))(self))
return _type
class obj__cards(DictElement):
item_xpath = 'comptePrincipal/cartesDD'
class item(ItemElement):
klass = Account
def obj_id(self):
return CleanText(Dict('idCarte'))(self).replace(' ', '')
obj_label = Format('Carte %s %s', Field('id'), CleanText(Dict('titulaire')))
obj_type = Account.TYPE_CARD
obj_coming = Eval(float_to_decimal, Dict('encoursCarteM'))
obj_balance = CleanDecimal(0)
obj__index = Dict('index')
obj__category = None
@method
class iter_accounts(DictElement):
item_xpath = 'grandesFamilles/*/elementsContrats'
class item(ItemElement):
IGNORED_ACCOUNTS = ("MES ASSURANCES",)
klass = Account
obj_id = CleanText(Dict('numeroCompteBam'))
obj_number = CleanText(Dict('numeroCompteBam'))
obj_label = CleanText(Dict('libelleProduit'))
obj_currency = CleanCurrency(Dict('idDevise'))
obj__index = Dict('index')
obj__category = Dict('grandeFamilleProduitCode', default=None)
obj__id_element_contrat = CleanText(Dict('idElementContrat'))
def obj_type(self):
_type = Map(CleanText(Dict('libelleUsuelProduit')), ACCOUNT_TYPES, Account.TYPE_UNKNOWN)(self)
if _type == Account.TYPE_UNKNOWN:
self.logger.warning('We got an untyped account: please add "%s" to ACCOUNT_TYPES.' % CleanText(Dict('libelleUsuelProduit'))(self))
return _type
def obj_balance(self):
balance = Dict('solde', default=None)(self)
if balance:
return Eval(float_to_decimal, balance)(self)
# We will fetch the balance with account_details
return NotAvailable
def condition(self):
# Ignore insurances (plus they all have identical IDs)
return CleanText(Dict('familleProduit/libelle', default=''))(self) not in self.IGNORED_ACCOUNTS
class AccountDetailsPage(LoggedPage, JsonPage):
def get_account_balances(self):
account_balances = {}
for el in self.doc:
value = el.get('solde', el.get('encoursActuel', el.get('valorisationContrat', el.get('montantRestantDu', el.get('capitalDisponible')))))
assert value is not None, 'Could not find the account balance'
account_balances[Dict('numeroCompte')(el)] = float_to_decimal(value)
return account_balances
def get_loan_ids(self):
loan_ids = {}
for el in self.doc:
if el.get('numeroCredit'):
# Loans
loan_ids[Dict('numeroCompte')(el)] = Dict('numeroCredit')(el)
elif el.get('numeroContrat'):
# Revolving credits
loan_ids[Dict('numeroCompte')(el)] = Dict('numeroContrat')(el)
return loan_ids
class IbanPage(LoggedPage, JsonPage):
def get_iban(self):
return Dict('ibanData/ibanCode', default=NotAvailable)(self.doc)
class HistoryPage(LoggedPage, JsonPage):
pass
class InvestmentPage(LoggedPage, JsonPage):
pass
class ProfilePage(LoggedPage, JsonPage):
pass
\ No newline at end of file
......@@ -28,7 +28,7 @@ from weboob.capabilities.profile import CapProfile
from weboob.tools.backend import Module, BackendConfig
from weboob.tools.value import ValueBackendPassword, Value
from .web.browser import Cragr
from .proxy_browser import ProxyBrowser
__all__ = ['CragrModule']
......@@ -82,10 +82,12 @@ class CragrModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapContact
'm.ca-valdefrance.fr': u'Val de France',
'm.lefil.com': u'Pyrénées Gascogne',
}.items())])
CONFIG = BackendConfig(Value('website', label=u'Région', choices=website_choices),
ValueBackendPassword('login', label=u'N° de compte', masked=False),
ValueBackendPassword('password', label=u'Code personnel', regexp=r'\d{6}'))
BROWSER = Cragr
BROWSER = ProxyBrowser
COMPAT_DOMAINS = {
'm.lefil.com': 'm.ca-pyrenees-gascogne.fr',
......
# -*- coding: utf-8 -*-
# Copyright(C) 2012-2019 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from weboob.browser.switch import SwitchingBrowser
from .api.browser import CragrAPI
from .web.browser import Cragr
class ProxyBrowser(SwitchingBrowser):
BROWSERS = {
'main': Cragr,
'api': CragrAPI,
}
......@@ -31,6 +31,7 @@ from weboob.capabilities.bank import (
from weboob.capabilities.base import find_object, empty
from weboob.capabilities.profile import ProfileMissing
from weboob.browser import LoginBrowser, URL, need_login, StatesMixin
from weboob.browser.switch import SiteSwitch
from weboob.browser.pages import FormNotFound
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable
from weboob.tools.date import ChaoticDateGuesser, LinearDateGuesser
......@@ -42,7 +43,7 @@ from weboob.tools.capabilities.bank.iban import is_iban_valid
from weboob.tools.capabilities.bank.investments import create_french_liquidity
from .pages import (
HomePage, LoginPage, LoginErrorPage, AccountsPage,
HomePage, NewWebsitePage, LoginPage, LoginErrorPage, AccountsPage,
SavingsPage, TransactionsPage, AdvisorPage, UselessPage,
CardsPage, LifeInsurancePage, MarketPage, LoansPage, PerimeterPage,
ChgPerimeterPage, MarketHomePage, FirstVisitPage, BGPIPage,
......@@ -61,6 +62,7 @@ class WebsiteNotSupported(Exception):
class Cragr(LoginBrowser, StatesMixin):
home_page = URL('/$', '/particuliers.html', 'https://www.*.fr/Vitrine/jsp/CMDS/b.js', HomePage)
new_website = URL(r'https://www.credit-agricole.fr/.*', NewWebsitePage)
login_page = URL(r'/stb/entreeBam$',
r'/stb/entreeBam\?.*typeAuthentification=CLIC_ALLER.*',
LoginPage)
......@@ -170,6 +172,10 @@ class Cragr(LoginBrowser, StatesMixin):
if not self.home_page.is_here():
self.home_page.go()
if self.new_website.is_here():
self.logger.warning('This connection uses the new API website')
raise SiteSwitch('api')
if self.new_login:
self.page.go_to_auth()
parsed = urlparse(self.url)
......
......@@ -145,6 +145,10 @@ class HomePage(BasePage):
return Regexp(CleanText('.'), r'public_key.+?(\w+)')(self.doc)
class NewWebsitePage(BasePage):
pass
class LoginPage(BasePage):
def on_load(self):
if self.doc.xpath('//font[@class="taille2"]'):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment