Skip to content
Commits on Source (2)
......@@ -20,28 +20,29 @@
from __future__ import unicode_literals
from random import randint
from weboob.browser import URL, LoginBrowser, need_login
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable
from weboob.browser.browsers import URL, LoginBrowser, need_login
from .compat.weboob_exceptions import BrowserIncorrectPassword, BrowserUnavailable, BrowserPasswordExpired
from weboob.tools.compat import basestring
from .pages import LoginPage, IndexPage, BadLogin, AccountDetailPage, AccountHistoryPage
from .pages import (
LoginPage, IndexPage, WrongPasswordPage, WrongWebsitePage,
AccountDetailPage, AccountHistoryPage, MigrationPage,
)
class AferBrowser(LoginBrowser):
BASEURL = 'https://adherent.gie-afer.fr'
login = URL('/web/ega.nsf/listeAdhesions\?OpenForm', LoginPage)
bad_login = URL('/names.nsf\?Login', BadLogin)
login = URL(r'/espaceadherent/MonCompte/Connexion$', LoginPage)
wrong_password = URL(r'/espaceadherent/MonCompte/Connexion\?err=6001', WrongPasswordPage)
wrong_website = URL(r'/espaceadherent/MonCompte/Connexion\?err=6008', WrongWebsitePage)
migration = URL(r'/espaceadherent/MonCompte/Migration', MigrationPage)
index = URL('/web/ega.nsf/listeAdhesions\?OpenForm', IndexPage)
account_detail = URL('/web/ega.nsf/soldeEpargne\?openForm', AccountDetailPage)
account_history = URL('/web/ega.nsf/generationSearchModule\?OpenAgent', AccountHistoryPage)
history_detail = URL('/web/ega.nsf/WOpendetailOperation\?OpenAgent', AccountHistoryPage)
def do_login(self):
"""
Attempt to log in.
Note: this method does nothing if we are already logged in.
"""
assert isinstance(self.username, basestring)
assert isinstance(self.password, basestring)
self.login.go()
......@@ -51,13 +52,14 @@ def do_login(self):
except BrowserUnavailable:
raise BrowserIncorrectPassword()
if self.bad_login.is_here():
if self.migration.is_here():
raise BrowserPasswordExpired(self.page.get_error())
if self.wrong_password.is_here():
error = self.page.get_error()
if "La saisie de l’identifiant ou du code confidentiel est incorrecte" in error or \
"Veuillez-vous identifier" in error:
if error:
raise BrowserIncorrectPassword(error)
else:
assert False, "Message d'erreur inconnu: %s" % error
assert False, 'We landed on WrongPasswordPage but no error message was fetched.'
@need_login
......
......@@ -27,6 +27,10 @@ class RecipientInvalidOTP(AddRecipientError):
code = 'invalidOTP'
class TransferInvalidOTP(TransferError):
code = 'invalidOTP'
class AccountOwnership(object):
"""
Relationship between the credentials owner (PSU) and the account
......@@ -43,6 +47,6 @@ class AccountOwnership(object):
try:
__all__ += ['AccountOwnership', 'RecipientInvalidOTP']
__all__ += ['AccountOwnership', 'RecipientInvalidOTP', 'TransferInvalidOTP']
except NameError:
pass
import weboob.exceptions as OLD
# can't import *, __all__ is incomplete...
for attr in dir(OLD):
globals()[attr] = getattr(OLD, attr)
try:
__all__ = OLD.__all__
except AttributeError:
pass
class BrowserInteraction(Exception):
pass
class BrowserQuestion(BrowserInteraction):
"""
When raised by a browser,
"""
def __init__(self, *fields):
self.fields = fields
class DecoupledValidation(BrowserInteraction):
def __init__(self, message='', resource=None, *values):
super(DecoupledValidation, self).__init__(*values)
self.message = message
self.resource = resource
def __str__(self):
return self.message
class AppValidation(DecoupledValidation):
pass
......@@ -39,8 +39,11 @@ class AferModule(Module, CapBankWealth):
VERSION = '1.5'
BROWSER = AferBrowser
CONFIG = BackendConfig(ValueBackendPassword('login', label='Username', regexp='[A-z]\d+', masked=False),
ValueBackendPassword('password', label=u"mdp", regexp='\d{1,8}'))
CONFIG = BackendConfig(
ValueBackendPassword('login', label='Identifiant', regexp=r'.+', masked=False),
ValueBackendPassword('password', label="Mot de passe", regexp=r'\d{1,8}|[a-zA-Z0-9]{7,30}')
# TODO lose previous regex (and in backend) once users credentials migration is complete
)
def create_default_browser(self):
return self.create_browser(self.config['login'].get(),
......
......@@ -28,23 +28,36 @@
from weboob.browser.pages import HTMLPage, LoggedPage, pagination
from .compat.weboob_capabilities_bank import Account, Investment, Transaction
from weboob.capabilities.base import NotAvailable
from weboob.exceptions import BrowserUnavailable, ActionNeeded
from .compat.weboob_exceptions import BrowserUnavailable, ActionNeeded
class LoginPage(HTMLPage):
def login(self, login, passwd):
form = self.get_form(name='_DominoForm')
form['Username'] = login
form = self.get_form(id='loginForm')
form['username'] = login
form['password'] = passwd
form.submit()
def is_here(self):
return bool(self.doc.xpath('//form[@name="_DominoForm"]'))
class WrongPasswordPage(HTMLPage):
def get_error(self):
return CleanText('//p[contains(text(), "Votre saisie est erronée")]')(self.doc)
class WrongWebsitePage(HTMLPage):
# We land on this page when the website indicates that
# an account is already created on the 'Aviva et moi' space,
# So we check the message and raise ActionNeeded with it
def on_load(self):
message = CleanText('//p[contains(text(), "Vous êtes déjà inscrit")]')(self.doc)
if message:
raise ActionNeeded(message)
assert False, 'We landed on WrongWebsitePage but no message was fetched.'
class BadLogin(HTMLPage):
class MigrationPage(HTMLPage):
def get_error(self):
return CleanText('//div[@id="idDivErrorLogin"]')(self.doc)
return CleanText('//h1[contains(text(), "Votre nouvel identifiant et mot de passe")]')(self.doc)
class IndexPage(LoggedPage, HTMLPage):
......
......@@ -20,8 +20,8 @@
from __future__ import unicode_literals
from datetime import date
from weboob.browser import LoginBrowser, URL, need_login, StatesMixin
from weboob.exceptions import (
from weboob.browser.browsers import LoginBrowser, URL, need_login, StatesMixin
from .compat.weboob_exceptions import (
BrowserIncorrectPassword, BrowserUnavailable, ImageCaptchaQuestion, BrowserQuestion, ActionNeeded,
WrongCaptchaResponse
)
......
import weboob.exceptions as OLD
# can't import *, __all__ is incomplete...
for attr in dir(OLD):
globals()[attr] = getattr(OLD, attr)
try:
__all__ = OLD.__all__
except AttributeError:
pass
class BrowserInteraction(Exception):
pass
class BrowserQuestion(BrowserInteraction):
"""
When raised by a browser,
"""
def __init__(self, *fields):
self.fields = fields
class DecoupledValidation(BrowserInteraction):
def __init__(self, message='', resource=None, *values):
super(DecoupledValidation, self).__init__(*values)
self.message = message
self.resource = resource
def __str__(self):
return self.message
class AppValidation(DecoupledValidation):
pass
......@@ -25,7 +25,7 @@
from weboob.browser.browsers import URL, LoginBrowser, need_login
from .compat.weboob_capabilities_bank import AccountNotFound
from weboob.exceptions import BrowserIncorrectPassword
from .compat.weboob_exceptions import BrowserIncorrectPassword
from weboob.tools.compat import unquote
from .pages import ActivityPage, SomePage, StatementPage, StatementsPage, SummaryPage
......
......@@ -27,6 +27,10 @@ class RecipientInvalidOTP(AddRecipientError):
code = 'invalidOTP'
class TransferInvalidOTP(TransferError):
code = 'invalidOTP'
class AccountOwnership(object):
"""
Relationship between the credentials owner (PSU) and the account
......@@ -43,6 +47,6 @@ class AccountOwnership(object):
try:
__all__ += ['AccountOwnership', 'RecipientInvalidOTP']
__all__ += ['AccountOwnership', 'RecipientInvalidOTP', 'TransferInvalidOTP']
except NameError:
pass
import weboob.exceptions as OLD
# can't import *, __all__ is incomplete...
for attr in dir(OLD):
globals()[attr] = getattr(OLD, attr)
try:
__all__ = OLD.__all__
except AttributeError:
pass
class BrowserInteraction(Exception):
pass
class BrowserQuestion(BrowserInteraction):
"""
When raised by a browser,
"""
def __init__(self, *fields):
self.fields = fields
class DecoupledValidation(BrowserInteraction):
def __init__(self, message='', resource=None, *values):
super(DecoupledValidation, self).__init__(*values)
self.message = message
self.resource = resource
def __str__(self):
return self.message
class AppValidation(DecoupledValidation):
pass
......@@ -42,7 +42,7 @@ def do_login(self):
@need_login
def iter_subscription(self):
self.subscription_page.go()
return self.page.iter_subscriptions()
yield self.page.get_subscription()
@need_login
def iter_documents(self, subscription):
......@@ -75,6 +75,6 @@ def iter_documents(self, subscription):
# then we set Rechercher to actionEvt to filter for this subscription, within last 6 months
# without first request we would have filter for this subscription but within last 2 months
params['actionEvt'] = 'Rechercher'
params['Beneficiaire'] = subscription._param
params['Beneficiaire'] = 'tout_selectionner'
self.documents_page.go(params=params)
return self.page.iter_documents(subid=subscription.id)
import weboob.exceptions as OLD
# can't import *, __all__ is incomplete...
for attr in dir(OLD):
globals()[attr] = getattr(OLD, attr)
try:
__all__ = OLD.__all__
except AttributeError:
pass
class BrowserInteraction(Exception):
pass
class BrowserQuestion(BrowserInteraction):
"""
When raised by a browser,
"""
def __init__(self, *fields):
self.fields = fields
class DecoupledValidation(BrowserInteraction):
def __init__(self, message='', resource=None, *values):
super(DecoupledValidation, self).__init__(*values)
self.message = message
self.resource = resource
def __str__(self):
return self.message
class AppValidation(DecoupledValidation):
pass
......@@ -19,15 +19,15 @@
from __future__ import unicode_literals
import re
from weboob.browser.elements import method, ListElement, ItemElement
from weboob.browser.filters.html import Attr, Link
from weboob.browser.filters.html import Link
from .compat.weboob_browser_filters_standard import CleanText, Regexp, CleanDecimal, Currency, Field, Format, Env
from weboob.browser.pages import LoggedPage, HTMLPage, PartialHTMLPage
from weboob.capabilities.bill import Subscription, Bill
from weboob.exceptions import BrowserUnavailable
from .compat.weboob_exceptions import BrowserUnavailable
from weboob.tools.date import parse_french_date
from weboob.tools.json import json
class LoginPage(HTMLPage):
......@@ -45,32 +45,22 @@ def on_load(self):
class SubscriptionPage(LoggedPage, HTMLPage):
@method
class iter_subscriptions(ListElement):
item_xpath = '//div[@id="corps-de-la-page"]//div[@class="tableau"]/div'
class item(ItemElement):
klass = Subscription
obj__labelid = Attr('.', 'aria-labelledby')
def get_subscription(self):
sub = Subscription()
# DON'T TAKE social security number for id because it's a very confidential data, take birth date instead
sub.id = CleanText('//button[@id="idAssure"]//td[@class="dateNaissance"]')(self.doc).replace('/', '')
sub.label = sub.subscriber = CleanText('//div[@id="pageAssure"]//span[@class="NomEtPrenomLabel"]')(self.doc)
def obj__birthdate(self):
return CleanText('//button[@id="%s"]//td[@class="dateNaissance"]' % Field('_labelid')(self))(self)
return sub
def obj_id(self):
# DON'T TAKE social security number for id because it's a very confidential data, take birth date instead
return ''.join(re.findall(r'\d+', Field('_birthdate')(self)))
def obj__param(self):
reversed_date = ''.join(reversed(re.findall(r'\d+', Field('_birthdate')(self))))
name = CleanText('//button[@id="%s"]//td[@class="nom"]' % Field('_labelid')(self))(self)
return '%s!-!%s!-!1' % (reversed_date, name)
obj_subscriber = CleanText('.//span[@class="NomEtPrenomLabel"]')
obj_label = obj_subscriber
class DocumentsPage(LoggedPage, PartialHTMLPage):
ENCODING = 'utf-8'
def build_doc(self, content):
res = json.loads(content)
return super(DocumentsPage, self).build_doc(res['tableauPaiement'].encode('utf-8'))
class DocumentsPage(LoggedPage, PartialHTMLPage):
@method
class iter_documents(ListElement):
item_xpath = '//ul[@id="unordered_list"]//li[has-class("rowitem")]'
......@@ -82,7 +72,7 @@ class item(ItemElement):
obj_label = CleanText('.//div[has-class("col-label")]')
obj_price = CleanDecimal.French('.//div[has-class("col-montant")]/span')
obj_currency = Currency('.//div[has-class("col-montant")]/span')
obj_url = Link('.//a[@class="downdetail"]')
obj_url = Link('.//div[@class="col-download"]/a')
obj_format = 'pdf'
def obj_date(self):
......
......@@ -17,8 +17,8 @@
# 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.browser import LoginBrowser, URL, need_login
from weboob.exceptions import BrowserIncorrectPassword
from weboob.browser.browsers import LoginBrowser, URL, need_login
from .compat.weboob_exceptions import BrowserIncorrectPassword
from weboob.capabilities.bill import Detail
from weboob.tools.compat import urlencode
from decimal import Decimal
......
import weboob.exceptions as OLD
# can't import *, __all__ is incomplete...
for attr in dir(OLD):
globals()[attr] = getattr(OLD, attr)
try:
__all__ = OLD.__all__
except AttributeError:
pass
class BrowserInteraction(Exception):
pass
class BrowserQuestion(BrowserInteraction):
"""
When raised by a browser,
"""
def __init__(self, *fields):
self.fields = fields
class DecoupledValidation(BrowserInteraction):
def __init__(self, message='', resource=None, *values):
super(DecoupledValidation, self).__init__(*values)
self.message = message
self.resource = resource
def __str__(self):
return self.message
class AppValidation(DecoupledValidation):
pass
......@@ -17,17 +17,20 @@
# 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 datetime
from weboob.exceptions import BrowserIncorrectPassword
from .compat.weboob_exceptions import BrowserIncorrectPassword
from weboob.browser.browsers import LoginBrowser, need_login
from weboob.browser.exceptions import HTTPNotFound, ServerError
from .compat.weboob_browser_url import URL
from dateutil.parser import parse as parse_date
from .pages import (
AccountsPage, JsonBalances, JsonPeriods, JsonHistory,
JsonBalances2, CurrencyPage, LoginPage, WrongLoginPage, AccountSuspendedPage,
NoCardPage, NotFoundPage
NoCardPage, NotFoundPage,
)
......@@ -49,16 +52,16 @@ class AmericanExpressBrowser(LoginBrowser):
js_posted = URL(r'/account-data/v1/financials/transactions\?limit=1000&offset=(?P<offset>\d+)&statement_end_date=(?P<end>[0-9-]+)&status=posted',
JsonHistory)
js_periods = URL(r'/account-data/v1/financials/statement_periods', JsonPeriods)
currency_page = URL(r'https://www.aexp-static.com/cdaas/axp-app/modules/axp-offers/1.11.1/fr-fr/axp-offers.json', CurrencyPage)
currency_page = URL(r'https://www.aexp-static.com/cdaas/axp-app/modules/axp-balance-summary/4.7.0/(?P<locale>\w\w-\w\w)/axp-balance-summary.json', CurrencyPage)
no_card = URL('https://www.americanexpress.com/us/content/no-card/',
'https://www.americanexpress.com/us/no-card/', NoCardPage)
no_card = URL(r'https://www.americanexpress.com/us/content/no-card/',
r'https://www.americanexpress.com/us/no-card/', NoCardPage)
not_found = URL(r'/accounts/error', NotFoundPage)
SUMMARY_CARD_LABEL = [
u'PAYMENT RECEIVED - THANK YOU',
u'PRELEVEMENT AUTOMATIQUE ENREGISTRE-MERCI'
'PAYMENT RECEIVED - THANK YOU',
'PRELEVEMENT AUTOMATIQUE ENREGISTRE-MERCI',
]
def __init__(self, *args, **kwargs):
......@@ -73,7 +76,6 @@ def do_login(self):
if self.wrong_login.is_here() or self.login.is_here() or self.account_suspended.is_here():
raise BrowserIncorrectPassword()
@need_login
def get_accounts(self):
self.accounts.go()
......@@ -91,7 +93,8 @@ def get_accounts(self):
self.page.set_balances(accounts)
# get currency
self.currency_page.go()
loc = self.session.cookies.get_dict(domain=".americanexpress.com")['axplocale'].lower()
self.currency_page.go(locale=loc)
currency = self.page.get_currency()
for acc in accounts:
......@@ -120,29 +123,30 @@ def iter_history(self, account):
@need_login
def iter_coming(self, account):
"""
Coming transactions can be found in a'pending' JSON if it exists (corresponding a 'Transactions en attente' tab on the website),
as well as in a 'posted' JSON (corresponding a 'Transactions enregistrées' tab on the website for futur transactions)
"""
# Coming transactions can be found in a 'pending' JSON if it exists
# ('En attente' tab on the website), as well as in a 'posted' JSON
# ('Enregistrées' tab on the website)
# "pending" have no vdate and debit date is in future
self.js_periods.go(headers={'account_token': account._token})
date = datetime.datetime.strptime(self.page.get_periods()[0], '%Y-%m-%d').date()
periods = self.page.get_periods()
date = parse_date(periods[0]).date()
today = datetime.date.today()
try:
self.js_pending.go(offset=0, headers={'account_token': account._token})
# when the latest period ends today we can't know the coming debit date
if date != today:
# when the latest period ends today we can't know the coming debit date
if date != today:
try:
self.js_pending.go(offset=0, headers={'account_token': account._token})
except ServerError as exc:
# At certain times of the month a connection might not have pendings;
# in that case, `js_pending.go` would throw a 502 error Bad Gateway
error_code = exc.response.json().get('code')
error_message = exc.response.json().get('message')
self.logger.warning('No pendings page to access to, got error %s and message "%s" instead.', error_code, error_message)
else:
for tr in self.page.iter_history(periods=periods):
if tr._owner == account._idforJSON:
tr.date = date
yield tr
except ServerError as exc:
# At certain time of the month a connection might not have pendings;
# in that case, `js_pending.go` would throw a 502 error Bad Gateway
error_code = exc.response.json().get('code')
error_message = exc.response.json().get('message')
self.logger.warning('No pendings page to access to, got error %s and message "%s" instead.' % (error_code, error_message))
# "posted" have a vdate but debit date can be future or past
for p in periods:
......
......@@ -27,6 +27,10 @@ class RecipientInvalidOTP(AddRecipientError):
code = 'invalidOTP'
class TransferInvalidOTP(TransferError):
code = 'invalidOTP'
class AccountOwnership(object):
"""
Relationship between the credentials owner (PSU) and the account
......@@ -43,6 +47,6 @@ class AccountOwnership(object):
try:
__all__ += ['AccountOwnership', 'RecipientInvalidOTP']
__all__ += ['AccountOwnership', 'RecipientInvalidOTP', 'TransferInvalidOTP']
except NameError:
pass
import weboob.exceptions as OLD
# can't import *, __all__ is incomplete...
for attr in dir(OLD):
globals()[attr] = getattr(OLD, attr)
try:
__all__ = OLD.__all__
except AttributeError:
pass
class BrowserInteraction(Exception):
pass
class BrowserQuestion(BrowserInteraction):
"""
When raised by a browser,
"""
def __init__(self, *fields):
self.fields = fields
class DecoupledValidation(BrowserInteraction):
def __init__(self, message='', resource=None, *values):
super(DecoupledValidation, self).__init__(*values)
self.message = message
self.resource = resource
def __str__(self):
return self.message
class AppValidation(DecoupledValidation):
pass
......@@ -22,7 +22,6 @@
from ast import literal_eval
from decimal import Decimal
import re
from dateutil.parser import parse as parse_date
from weboob.browser.pages import LoggedPage, JsonPage, HTMLPage
from weboob.browser.elements import ItemElement, DictElement, method
......@@ -32,12 +31,14 @@
from weboob.capabilities.base import NotAvailable
from weboob.tools.json import json
from weboob.tools.compat import basestring
from weboob.exceptions import ActionNeeded, BrowserUnavailable
from .compat.weboob_exceptions import ActionNeeded, BrowserUnavailable
from dateutil.parser import parse as parse_date
def float_to_decimal(f):
return Decimal(str(f))
def parse_decimal(s):
# we might get 1,399,680 in rupie indonésienne
if s.count(',') > 1 and not s.count('.'):
......@@ -87,13 +88,13 @@ def iter_accounts(self):
else:
assert False, "data was not found"
assert data[13] == 'core'
assert len(data[14]) == 3
assert data[15] == 'core'
assert len(data[16]) == 3
# search for products to get products list
for index, el in enumerate(data[14][2]):
for index, el in enumerate(data[16][2]):
if 'products' in el:
accounts_data = data[14][2][index+1]
accounts_data = data[16][2][index + 1]
assert len(accounts_data) == 2
assert accounts_data[1][4] == 'productsList'
......@@ -105,14 +106,14 @@ def iter_accounts(self):
if isinstance(account_data, basestring):
balances_token = account_data
elif isinstance(account_data, list) and not account_data[4][2][0]=="Canceled":
elif isinstance(account_data, list) and not account_data[4][2][0] == "Canceled":
acc = Account()
if len(account_data) > 15:
token.append(account_data[-11])
acc._idforJSON = account_data[10][-1]
acc._idforJSON = account_data[10][-1]
else:
acc._idforJSON = account_data[-5][-1]
acc._idforJSON = re.sub('\s+', ' ', acc._idforJSON)
acc._idforJSON = re.sub(r'\s+', ' ', acc._idforJSON)
acc.number = '-%s' % account_data[2][2]
acc.label = '%s %s' % (account_data[6][4], account_data[10][-1])
acc._balances_token = acc.id = balances_token
......@@ -142,7 +143,7 @@ def set_balances(self, accounts):
class CurrencyPage(LoggedPage, JsonPage):
def get_currency(self):
return self.doc['currency']
return self.doc['localeSettings']['currency_code']
class JsonPeriods(LoggedPage, JsonPage):
......@@ -172,7 +173,8 @@ def obj_type(self):
obj_raw = CleanText(Dict('description', default=''))
def obj_date(self):
""" 'statement_end_date' might be absent from this json, we must match the rdate with the right date period """
# 'statement_end_date' might be absent from this json,
# we must match the rdate with the right date period
_date = Date(Dict('statement_end_date', default=None), default=NotAvailable)(self)
if not _date:
periods = Env('periods')(self)
......@@ -207,6 +209,4 @@ def obj_original_amount(self):
else:
return original_amount
# obj__ref = Dict('reference_id')
obj__ref = Dict('identifier')