pax_global_header 0000666 0000000 0000000 00000000064 13400741301 0014503 g ustar 00root root 0000000 0000000 52 comment=309ae667efa3c8f7192ef6b744c295fbb8a2d101
woob-309ae667efa3c8f7192ef6b744c295fbb8a2d101-modules-fortuneo/ 0000775 0000000 0000000 00000000000 13400741301 0023047 5 ustar 00root root 0000000 0000000 woob-309ae667efa3c8f7192ef6b744c295fbb8a2d101-modules-fortuneo/modules/ 0000775 0000000 0000000 00000000000 13400741301 0024517 5 ustar 00root root 0000000 0000000 woob-309ae667efa3c8f7192ef6b744c295fbb8a2d101-modules-fortuneo/modules/fortuneo/ 0000775 0000000 0000000 00000000000 13400741301 0026360 5 ustar 00root root 0000000 0000000 woob-309ae667efa3c8f7192ef6b744c295fbb8a2d101-modules-fortuneo/modules/fortuneo/__init__.py 0000664 0000000 0000000 00000001467 13400741301 0030501 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Gilles-Alexandre Quenot
#
# 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 .
from .module import FortuneoModule
__all__ = ['FortuneoModule']
# vim:ts=4:sw=4
woob-309ae667efa3c8f7192ef6b744c295fbb8a2d101-modules-fortuneo/modules/fortuneo/browser.py 0000664 0000000 0000000 00000027125 13400741301 0030424 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Gilles-Alexandre Quenot
#
# 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 .
from __future__ import unicode_literals
import time
import json
from datetime import datetime, timedelta
from weboob.browser import LoginBrowser, URL, need_login, StatesMixin
from weboob.exceptions import AuthMethodNotImplemented, BrowserIncorrectPassword, ActionNeeded
from weboob.capabilities.bank import Account, AddRecipientStep, Recipient
from weboob.tools.capabilities.bank.transactions import sorted_transactions
from weboob.tools.value import Value
from .pages.login import LoginPage, UnavailablePage
from .pages.accounts_list import (
AccountsList, AccountHistoryPage, CardHistoryPage, InvestmentHistoryPage, PeaHistoryPage, LoanPage, ProfilePage, ProfilePageCSV, SecurityPage,
)
from .pages.transfer import (
RegisterTransferPage, ValidateTransferPage, ConfirmTransferPage, RecipientsPage, RecipientSMSPage
)
__all__ = ['Fortuneo']
class Fortuneo(LoginBrowser, StatesMixin):
BASEURL = 'https://mabanque.fortuneo.fr'
STATE_DURATION = 5
login_page = URL(r'.*identification\.jsp.*', LoginPage)
accounts_page = URL(r'/fr/prive/default.jsp\?ANav=1',
r'.*prive/default\.jsp.*',
r'.*/prive/mes-comptes/synthese-mes-comptes\.jsp',
AccountsList)
account_history = URL(r'.*/prive/mes-comptes/livret/consulter-situation/consulter-solde\.jsp.*',
r'.*/prive/mes-comptes/compte-courant/consulter-situation/consulter-solde\.jsp.*',
r'.*/prive/mes-comptes/compte-especes.*',
AccountHistoryPage)
card_history = URL(r'.*/prive/mes-comptes/compte-courant/carte-bancaire/encours-debit-differe\.jsp.*', CardHistoryPage)
pea_history = URL(r'.*/prive/mes-comptes/pea/.*',
r'.*/prive/mes-comptes/compte-titres-pea/.*',
r'.*/prive/mes-comptes/ppe/.*', PeaHistoryPage)
invest_history = URL(r'.*/prive/mes-comptes/assurance-vie/.*', InvestmentHistoryPage)
loan_contract = URL(r'/fr/prive/mes-comptes/credit-immo/contrat-credit-immo/contrat-pret-immobilier.jsp.*', LoanPage)
unavailable = URL(r'/customError/indispo.html', UnavailablePage)
security_page = URL(r'/fr/prive/identification-carte-securite-forte.jsp.*', SecurityPage)
# transfer
recipients = URL(
r'/fr/prive/mes-comptes/compte-courant/realiser-operations/gerer-comptes-externes/consulter-comptes-externes.jsp',
r'/fr/prive/verifier-compte-externe.jsp',
r'fr/prive/mes-comptes/compte-courant/.*/gestion-comptes-externes.jsp',
RecipientsPage)
recipient_sms = URL(
r'/fr/prive/appel-securite-forte-otp-bankone.jsp',
r'/fr/prive/mes-comptes/compte-courant/.*/confirmer-ajout-compte-externe.jsp',
RecipientSMSPage)
register_transfer = URL(
r'/fr/prive/mes-comptes/compte-courant/realiser-operations/saisie-virement.jsp\?ca=(?P)',
RegisterTransferPage)
validate_transfer = URL(
r'/fr/prive/mes-comptes/compte-courant/.*/verifier-saisie-virement.jsp',
ValidateTransferPage)
confirm_transfer = URL(
r'fr/prive/mes-comptes/compte-courant/.*/init-confirmer-saisie-virement.jsp',
r'/fr/prive/mes-comptes/compte-courant/.*/confirmer-saisie-virement.jsp',
ConfirmTransferPage)
profile = URL(r'/fr/prive/informations-client.jsp', ProfilePage)
profile_csv = URL(r'/PdfStruts\?*', ProfilePageCSV)
need_reload_state = None
__states__ = ['need_reload_state', 'add_recipient_form']
def __init__(self, *args, **kwargs):
LoginBrowser.__init__(self, *args, **kwargs)
self.investments = {}
self.action_needed_processed = False
self.add_recipient_form = None
def do_login(self):
if not self.login_page.is_here():
self.location('/fr/identification.jsp')
self.page.login(self.username, self.password)
if self.login_page.is_here():
raise BrowserIncorrectPassword()
self.location('/fr/prive/default.jsp?ANav=1')
if self.accounts_page.is_here() and self.page.need_sms():
raise AuthMethodNotImplemented('Authentification with sms is not supported')
def load_state(self, state):
# reload state only for new recipient feature
if state.get('need_reload_state'):
# don't use locate browser for add recipient step
state.pop('url', None)
super(Fortuneo, self).load_state(state)
@need_login
def get_investments(self, account):
if hasattr(account, '_investment_link'):
if account.id in self.investments:
return self.investments[account.id]
else:
self.location(account._investment_link)
return self.page.get_investments(account)
return []
@need_login
def get_history(self, account):
self.location(account._history_link)
if not account.type == Account.TYPE_LOAN:
if self.page.select_period():
return sorted_transactions(self.page.get_operations())
return []
@need_login
def get_coming(self, account):
for cb_link in account._card_links:
for _ in range(3):
self.location(cb_link)
if not self.page.is_loading():
break
time.sleep(1)
for tr in sorted_transactions(self.page.get_operations()):
yield tr
@need_login
def get_accounts_list(self):
self.accounts_page.go()
# Note: if you want to debug process_action_needed() here,
# you must first set self.action_needed_processed to False
# otherwise it might not enter the "if" loop here below.
if not self.action_needed_processed:
self.process_action_needed()
assert self.accounts_page.is_here()
return self.page.get_list()
def process_action_needed(self):
# we have to go in an iframe to know if there are CGUs
url = self.page.get_iframe_url()
if url:
self.location(self.absurl(url, base=True)) # beware, the landing page might vary according to the referer page. So far I didn't figure out how the landing page is chosen.
if self.security_page.is_here():
# Some connections require reinforced security and we cannot bypass the OTP in order
# to get to the account information. Users have to provide a phone number in order to
# validate an OTP, so we must raise an ActionNeeded with the appropriate message.
raise ActionNeeded('Cette opération sensible doit être validée par un code sécurité envoyé par SMS ou serveur vocal. '
'Veuillez contacter le Service Clients pour renseigner vos coordonnées téléphoniques.')
# if there are skippable CGUs, skip them
if self.accounts_page.is_here() and self.page.has_action_needed():
# Look for the request in the event listener registered to the button
# can be harcoded, no variable part. It is a POST request without data.
self.location(self.absurl('ReloadContext?action=1&', base=True), method='POST')
self.accounts_page.go() # go back to the accounts page whenever there was an iframe or not
self.action_needed_processed = True
@need_login
def iter_recipients(self, origin_account):
self.register_transfer.go(ca=origin_account._ca)
if self.page.is_account_transferable(origin_account):
for internal_recipient in self.page.iter_internal_recipients(origin_account_id=origin_account.id):
yield internal_recipient
self.recipients.go()
for external_recipients in self.page.iter_external_recipients():
yield external_recipients
def copy_recipient(self, recipient):
rcpt = Recipient()
rcpt.iban = recipient.iban
rcpt.id = recipient.iban
rcpt.label = recipient.label
rcpt.category = recipient.category
rcpt.enabled_at = datetime.now().replace(microsecond=0) + timedelta(days=1)
rcpt.currency = u'EUR'
return rcpt
def new_recipient(self, recipient, **params):
if 'code' in params:
self.need_reload_state = None
# to drop and use self.add_recipient_form instead in send_code()
recipient_form = json.loads(self.add_recipient_form)
self.send_code(recipient_form ,params['code'])
assert self.page.rcpt_after_sms()
return self.copy_recipient(recipient)
return self.new_recipient_before_otp(recipient, **params)
@need_login
def new_recipient_before_otp(self, recipient, **params):
self.recipients.go()
self.page.check_external_iban_form(recipient)
self.page.check_recipient_iban()
# fill form
self.page.fill_recipient_form(recipient)
rcpt = self.page.get_new_recipient(recipient)
# get first part of confirm form
send_code_form = self.page.get_send_code_form()
data = {
'appelAjax': 'true',
'domicileUpdated': 'false',
'numeroSelectionne.value': '',
'portableUpdated': 'false',
'proUpdated': 'false',
'typeOperationSensible': 'AJOUT_BENEFICIAIRE'
}
# this send sms to user
self.location(self.absurl('/fr/prive/appel-securite-forte-otp-bankone.jsp', base=True) , data=data)
# get second part of confirm form
send_code_form.update(self.page.get_send_code_form_input())
# save form value and url for statesmixin
self.add_recipient_form = dict(send_code_form)
self.add_recipient_form.update({'url': send_code_form.url})
# storage can't handle dict with '.' in key
# to drop when dict with '.' in key is handled
self.add_recipient_form = json.dumps(self.add_recipient_form)
self.need_reload_state = True
raise AddRecipientStep(rcpt, Value('code', label='Veuillez saisir le code reçu.'))
def send_code(self, form_data, code):
form_url = form_data['url']
form_data['otp'] = code
form_data.pop('url')
self.location(self.absurl(form_url, base=True), data=form_data)
@need_login
def init_transfer(self, account, recipient, amount, label, exec_date):
self.register_transfer.go(ca=account._ca)
self.page.fill_transfer_form(account, recipient, amount, label, exec_date)
return self.page.handle_response(account, recipient, amount, label, exec_date)
@need_login
def execute_transfer(self, transfer):
self.page.validate_transfer()
self.page.confirm_transfer()
return self.page.transfer_confirmation(transfer)
@need_login
def get_profile(self):
self.profile.go()
csv_link = self.page.get_csv_link()
if csv_link:
self.location(csv_link)
return self.page.get_profile()
woob-309ae667efa3c8f7192ef6b744c295fbb8a2d101-modules-fortuneo/modules/fortuneo/favicon.png 0000664 0000000 0000000 00000003617 13400741301 0030522 0 ustar 00root root 0000000 0000000 PNG
IHDR @ @ iq sRGB bKGD pHYs tIME
.)jsD/ tEXtComment Created with GIMPW IDATxkLSgZ)¡AE[o V`ls`f`Mݖ}pYL4.Qc,̰6cpNQf8墴@ZZPZFyO/}=}~s?EoOS.Q Q Q Q Q Q Q O>1v7lV~#>
h!RT*
-z X
N4TvVkBPɐWХd̰iwGa;`;Хwft\L-Q]7Jlu'pR?|Ɍ
U"i! FC
UfNO`T)F6e؉W Pxl(`30h4湞ѥrpt.~
s F/ <K?`y>:^K? vz~݊FtDt휑!jmCv Ȯιh c
+9wɞ02y2[a}|^ PbZ9bZ9M$fCO Y#B8ܬz|]j2h58Zx*)ht2N
Ixoo;"0qy}QIۭF\Bt }{+.9%xldJ&9uۇY
& $G5z ",.~fzh]0 è;i-7DD+@^YFX@L'Ms6O}W0<^>16y}+AayerNQXvnȌ/\Lܨ@_*SKCKmJ"W(nS5 \H@kvxpPJ
Y+ibbZRBXϪfJ
1".nzoW dRpئFJ1V@!)C̞H|@˥^Tv}Cs$yyo{17+:̷L- ZS-P.fX
95~dhBI),Q-Җ'zq {{22 )* 6|v>@M`Z9
[+`/nP>$6TzI(:m"SXgMs![ !
3=Ƚfev[dۭ(!!!/j ptUO