Commit b55030c3 authored by Quentin Defenouillere's avatar Quentin Defenouillere Committed by Romain Bignon

[cragr] Corrected duplicate Loan IDs and iter_cards

In the accounts JSON, some Loans may have identical IDs, so we fetch the
id_element_contrat instead since it is always unique. This value will be
used in the details JSOn to fetch a unique ID for Loans, as well as the
missing account balances.
I also re-wrote iter_cards to fetch all cards (not just cards from the
main_account) and easily sort between immediate and deferred cards.

Closes: 35420@Sibi
parent 47e5bf4f
......@@ -20,20 +20,20 @@
from __future__ import unicode_literals
from decimal import Decimal
import re
from weboob.capabilities.bank import (
Account,
)
from weboob.capabilities.base import find_object, empty, NotAvailable
from weboob.capabilities.bank import Account
from weboob.capabilities.base import empty, NotAvailable
from weboob.browser import LoginBrowser, URL, need_login
from weboob.exceptions import BrowserUnavailable, BrowserIncorrectPassword
from weboob.exceptions import BrowserUnavailable, BrowserIncorrectPassword, ActionNeeded
from weboob.browser.exceptions import ServerError
from weboob.capabilities.bank import Loan
from weboob.tools.capabilities.bank.iban import is_iban_valid
from .pages import (
LoginPage, LoggedOutPage, KeypadPage, SecurityPage, ContractsPage, AccountsPage, AccountDetailsPage,
IbanPage, HistoryPage, ProfilePage,
IbanPage, CardsPage, ProfilePage,
)
......@@ -62,9 +62,9 @@ class CragrAPI(LoginBrowser):
r'association/operations/operations-courantes/editer-rib/jcr:content.ibaninformation.json',
r'professionnel/operations/operations-courantes/editer-rib/jcr:content.ibaninformation.json', IbanPage)
history_page = URL(r'particulier/operations/synthese/detail-comptes/jcr:content.n3.compte.infos.json',
r'association/operations/synthese/detail-comptes/jcr:content.n3.compte.infos.json',
r'professionnel/operations/synthese/detail-comptes/jcr:content.n3.compte.infos.json', HistoryPage)
cards = URL(r'particulier/operations/moyens-paiement/mes-cartes/jcr:content.listeCartesParCompte.json',
r'association/operations/moyens-paiement/mes-cartes/jcr:content.listeCartesParCompte.json',
r'professionnel/operations/moyens-paiement/mes-cartes/jcr:content.listeCartesParCompte.json', CardsPage)
profile_page = URL(r'particulier/operations/synthese/jcr:content.npc.store.client.json',
r'association/operations/synthese/jcr:content.npc.store.client.json',
......@@ -97,6 +97,18 @@ class CragrAPI(LoginBrowser):
message = error.get('message', '')
if 'Votre identification est incorrecte' in message:
raise BrowserIncorrectPassword()
if 'obtenir un nouveau code' in message:
raise ActionNeeded(message)
elif 'Un incident technique' in message:
# If it is a technical error, we try login again
try:
self.security_check.go(data=form)
except ServerError as exc:
error = exc.response.json().get('error')
if error:
message = error.get('message', '')
if 'Un incident technique' in message:
raise BrowserUnavailable(message)
assert False, 'Unhandled Server Error encountered: %s' % error.get('message', '')
# accounts_url may contain '/particulier', '/professionnel' or '/association'
......@@ -113,6 +125,10 @@ class CragrAPI(LoginBrowser):
total_spaces = self.page.count_spaces()
self.logger.info('The total number of spaces on this connection is %s.' % total_spaces)
# Complete accounts list is required to match card parent accounts
# and to avoid accounts that are present on several spaces
all_accounts = {}
for contract in range(total_spaces):
# This request often returns a 500 error so we retry several times.
try:
......@@ -135,8 +151,8 @@ class CragrAPI(LoginBrowser):
account._contract = contract
account.owner_type = self.page.get_owner_type()
# Some accounts have no balance in the main JSON, so we must
# get all the (id, balance) pairs in the account_details JSON:
# Some accounts have no balance in the main JSON, so we must get all
# the (_id_element_contrat, balance) pairs in the account_details JSON:
categories = {int(account._category) for account in accounts_list if account._category != None}
account_balances = {}
loan_ids = {}
......@@ -145,7 +161,6 @@ class CragrAPI(LoginBrowser):
account_balances.update(self.page.get_account_balances())
loan_ids.update(self.page.get_loan_ids())
# Getting IBANs for checking accounts
if main_account.type == Account.TYPE_CHECKING:
params = {
'compteIdx': int(main_account._index),
......@@ -155,43 +170,64 @@ class CragrAPI(LoginBrowser):
iban = self.page.get_iban()
if is_iban_valid(iban):
main_account.iban = iban
yield main_account
for card in main_account._cards:
card.parent = main_account
card.currency = main_account.currency
card.owner_type = main_account.owner_type
card._contract = contract
yield card
if main_account.id not in all_accounts:
all_accounts[main_account.id] = main_account
yield main_account
for account in accounts_list:
if empty(account.balance):
account.balance = account_balances.get(account.id, NotAvailable)
account.balance = account_balances.get(account._id_element_contrat, NotAvailable)
if account.type == Account.TYPE_CHECKING:
try:
params = {
'compteIdx': int(account._index),
'grandeFamilleCode': 1,
}
self.account_iban.go(params=params)
iban = self.page.get_iban()
if is_iban_valid(iban):
account.iban = iban
except ServerError:
self.logger.warning('Could not fetch IBAN for checking account "%s %s"', account.label, account.id)
pass
# TO-DO: Create Loan() object with its related attributes
params = {
'compteIdx': int(account._index),
'grandeFamilleCode': int(account._category),
}
self.account_iban.go(params=params)
iban = self.page.get_iban()
if is_iban_valid(iban):
account.iban = iban
# Loans have a specific ID that we need to fetch
# so the backend can match loans properly.
# If no there is no loan ID, we keep the account ID.
if account.type == Account.TYPE_LOAN:
account.id = loan_ids.get(account.id, account.id)
account.balance = -account.balance
account.id = account.number = loan_ids.get(account._id_element_contrat, account.id)
account = self.switch_account_to_loan(account)
elif account.type == Account.TYPE_REVOLVING_CREDIT:
account.id = loan_ids.get(account.id, account.id)
account.balance = 0
yield account
account.id = account.number = loan_ids.get(account._id_element_contrat, account.id)
account = self.switch_account_to_revolving(account)
if account.id not in all_accounts:
all_accounts[account.id] = account
yield account
# Fetch all deferred credit cards for this space
self.cards.go()
for card in self.page.iter_card_parents():
card.number = card.id
card.parent = all_accounts.get(card._parent_id, NotAvailable)
card.currency = card.parent.currency
card.owner_type = card.parent.owner_type
card._category = card.parent._category
card._contract = contract
if card.id not in all_accounts:
all_accounts[card.id] = card
yield card
def switch_account_to_loan(self, account):
loan = Loan()
copy_attrs = ('id', 'number', 'label', 'type', 'currency', '_index', '_category', '_contract', '_id_element_contrat', 'owner_type')
for attr in copy_attrs:
setattr(loan, attr, getattr(account, attr))
loan.balance = -account.balance
return loan
def switch_account_to_revolving(self, account):
loan = Loan()
copy_attrs = ('id', 'number', 'label', 'type', 'currency', '_index', '_category', '_contract', '_id_element_contrat', 'owner_type')
for attr in copy_attrs:
setattr(loan, attr, getattr(account, attr))
loan.balance = Decimal(0)
loan.available_amount = account.balance
return loan
@need_login
def go_to_account_space(self, contract):
......@@ -200,15 +236,6 @@ class CragrAPI(LoginBrowser):
self.contracts_page.go(id_contract=contract)
assert self.accounts_page.is_here()
@need_login
def get_card(self, id):
return find_object(self.get_cards(), id=id)
@need_login
def get_cards(self, accounts_list=None):
# accounts_list is only used by get_list
raise BrowserUnavailable()
@need_login
def get_history(self, account):
raise BrowserUnavailable()
......
......@@ -32,7 +32,7 @@ from weboob.capabilities.bank import (
from weboob.browser.elements import DictElement, ItemElement, method
from weboob.browser.filters.standard import (
CleanText, CleanDecimal, Currency as CleanCurrency, Format, Field, Map, Eval
CleanText, CleanDecimal, Currency as CleanCurrency, Format, Field, Map, Eval, Env,
)
from weboob.browser.filters.json import Dict
......@@ -102,12 +102,14 @@ ACCOUNT_TYPES = {
'DAV TIGERE': Account.TYPE_SAVINGS,
'CPTEXCPRO': Account.TYPE_SAVINGS,
'CPTEXCENT': Account.TYPE_SAVINGS,
'DAT': 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,
'CT ATT HAB': Account.TYPE_LOAN,
'PEA': Account.TYPE_PEA,
'PEAP': Account.TYPE_PEA,
'DAV PEA': Account.TYPE_PEA,
......@@ -165,7 +167,7 @@ class AccountsPage(LoggedPage, JsonPage):
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__category = Dict('comptePrincipal/grandeFamilleProduitCode', default=None)
obj__id_element_contrat = CleanText(Dict('comptePrincipal/idElementContrat'))
def obj_type(self):
......@@ -174,22 +176,6 @@ class AccountsPage(LoggedPage, JsonPage):
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'
......@@ -199,8 +185,13 @@ class AccountsPage(LoggedPage, JsonPage):
klass = Account
obj_id = CleanText(Dict('numeroCompteBam'))
obj_number = CleanText(Dict('numeroCompteBam'))
def obj_id(self):
# Loan ids may be duplicated so we use the contract number for now:
if Field('type')(self) == Account.TYPE_LOAN:
return CleanText(Dict('idElementContrat'))(self)
return CleanText(Dict('numeroCompte'))(self)
obj_number = CleanText(Dict('numeroCompte'))
obj_label = CleanText(Dict('libelleProduit'))
obj_currency = CleanCurrency(Dict('idDevise'))
obj__index = Dict('index')
......@@ -210,7 +201,7 @@ class AccountsPage(LoggedPage, JsonPage):
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))
self.logger.warning('There is an untyped account: please add "%s" to ACCOUNT_TYPES.' % CleanText(Dict('libelleUsuelProduit'))(self))
return _type
def obj_balance(self):
......@@ -227,22 +218,26 @@ class AccountsPage(LoggedPage, JsonPage):
class AccountDetailsPage(LoggedPage, JsonPage):
def get_account_balances(self):
# We use the 'idElementContrat' key because it is unique
# whereas the account id may not be unique for Loans
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)
account_balances[Dict('idElementContrat')(el)] = float_to_decimal(value)
return account_balances
def get_loan_ids(self):
# We use the 'idElementContrat' key because it is unique
# whereas the account id may not be unique for Loans
loan_ids = {}
for el in self.doc:
if el.get('numeroCredit'):
# Loans
loan_ids[Dict('numeroCompte')(el)] = Dict('numeroCredit')(el)
loan_ids[Dict('idElementContrat')(el)] = Dict('numeroCredit')(el)
elif el.get('numeroContrat'):
# Revolving credits
loan_ids[Dict('numeroCompte')(el)] = Dict('numeroContrat')(el)
loan_ids[Dict('idElementContrat')(el)] = Dict('numeroContrat')(el)
return loan_ids
......@@ -255,6 +250,40 @@ class HistoryPage(LoggedPage, JsonPage):
pass
class CardsPage(LoggedPage, JsonPage):
@method
class iter_card_parents(DictElement):
item_xpath = 'comptes'
class iter_cards(DictElement):
item_xpath = 'listeCartes'
def parse(self, el):
self.env['parent_id'] = Dict('idCompte')(el)
class item(ItemElement):
klass = Account
def obj_id(self):
return CleanText(Dict('idCarte'))(self).replace(' ', '')
def condition(self):
assert CleanText(Dict('codeTypeDebitPaiementCarte'))(self) in ('D', 'I')
return CleanText(Dict('codeTypeDebitPaiementCarte'))(self)=='D'
obj_label = Format('Carte %s %s', Field('id'), CleanText(Dict('titulaire')))
obj_type = Account.TYPE_CARD
obj_coming = Eval(lambda x: -float_to_decimal(x), Dict('encoursCarteM'))
obj_balance = CleanDecimal(0)
obj__parent_id = Env('parent_id')
obj__index = Dict('index')
obj__id_element_contrat = None
class CardHistoryPage(LoggedPage, JsonPage):
pass
class InvestmentPage(LoggedPage, JsonPage):
pass
......
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