# -*- coding: utf-8 -*- # Copyright(C) 2012-2019 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 Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this weboob module. If not, see . from __future__ import unicode_literals from decimal import Decimal import re from weboob.capabilities.bank import Account, Transaction, AccountNotFound, RecipientNotFound from weboob.capabilities.base import empty, NotAvailable, strict_find_object from weboob.browser import LoginBrowser, URL, need_login from weboob.exceptions import BrowserUnavailable, BrowserIncorrectPassword, ActionNeeded from weboob.browser.exceptions import ServerError, ClientError, BrowserHTTPNotFound from weboob.capabilities.bank import Loan from weboob.tools.capabilities.bank.iban import is_iban_valid from weboob.tools.capabilities.bank.transactions import sorted_transactions from .pages import ( LoginPage, LoggedOutPage, KeypadPage, SecurityPage, ContractsPage, FirstConnectionPage, AccountsPage, AccountDetailsPage, TokenPage, IbanPage, HistoryPage, CardsPage, CardHistoryPage, NetfincaRedirectionPage, PredicaRedirectionPage, PredicaInvestmentsPage, ProfilePage, ProfileDetailsPage, ProProfileDetailsPage, ) from .transfer_pages import ( RecipientsPage, TransferPage, TransferTokenPage, ) from weboob.tools.capabilities.bank.investments import create_french_liquidity from .netfinca_browser import NetfincaBrowser __all__ = ['CragrAPI'] class CragrAPI(LoginBrowser): login_page = URL(r'particulier/acceder-a-mes-comptes.html$', LoginPage) keypad = URL(r'particulier/acceder-a-mes-comptes.authenticationKeypad.json', KeypadPage) security_check = URL(r'particulier/acceder-a-mes-comptes.html/j_security_check', SecurityPage) first_connection = URL(r'.*/operations/interstitielles/premiere-connexion.html', FirstConnectionPage) logged_out = URL(r'.*', LoggedOutPage) token_page = URL(r'libs/granite/csrf/token.json', TokenPage) contracts_page = URL(r'particulier/operations/.rechargement.contexte.html\?idBamIndex=(?P)', r'association/operations/.rechargement.contexte.html\?idBamIndex=(?P)', r'professionnel/operations/.rechargement.contexte.html\?idBamIndex=(?P)', r'agriculteur/operations/.rechargement.contexte.html\?idBamIndex=(?P)', r'entreprise/operations/.rechargement.contexte.html\?idBamIndex=(?P)', ContractsPage) accounts_page = URL(r'particulier/operations/synthese.html', r'association/operations/synthese.html', r'professionnel/operations/synthese.html', r'agriculteur/operations/synthese.html', r'entreprise/operations/synthese.html', AccountsPage) account_details = URL(r'particulier/operations/synthese/jcr:content.produits-valorisation.json/(?P)', r'association/operations/synthese/jcr:content.produits-valorisation.json/(?P)', r'professionnel/operations/synthese/jcr:content.produits-valorisation.json/(?P)', r'agriculteur/operations/synthese/jcr:content.produits-valorisation.json/(?P)', r'entreprise/operations/synthese/jcr:content.produits-valorisation.json/(?P)', AccountDetailsPage) account_iban = URL(r'particulier/operations/operations-courantes/editer-rib/jcr:content.ibaninformation.json', r'association/operations/operations-courantes/editer-rib/jcr:content.ibaninformation.json', r'professionnel/operations/operations-courantes/editer-rib/jcr:content.ibaninformation.json', r'agriculteur/operations/operations-courantes/editer-rib/jcr:content.ibaninformation.json', r'entreprise/operations/operations-courantes/editer-rib/jcr:content.ibaninformation.json', IbanPage) 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', r'agriculteur/operations/moyens-paiement/mes-cartes/jcr:content.listeCartesParCompte.json', r'entreprise/operations/moyens-paiement/mes-cartes/jcr:content.listeCartesParCompte.json', CardsPage) history = URL(r'particulier/operations/synthese/detail-comptes/jcr:content.n3.operations.json', r'association/operations/synthese/detail-comptes/jcr:content.n3.operations.json', r'professionnel/operations/synthese/detail-comptes/jcr:content.n3.operations.json', r'agriculteur/operations/synthese/detail-comptes/jcr:content.n3.operations.json', r'entreprise/operations/synthese/detail-comptes/jcr:content.n3.operations.json', HistoryPage) card_history = URL(r'particulier/operations/synthese/detail-comptes/jcr:content.n3.operations.encours.carte.debit.differe.json', r'association/operations/synthese/detail-comptes/jcr:content.n3.operations.encours.carte.debit.differe.json', r'professionnel/operations/synthese/detail-comptes/jcr:content.n3.operations.encours.carte.debit.differe.json', r'agriculteur/operations/synthese/detail-comptes/jcr:content.n3.operations.encours.carte.debit.differe.json', r'entreprise/operations/synthese/detail-comptes/jcr:content.n3.operations.encours.carte.debit.differe.json', CardHistoryPage) netfinca_redirection = URL(r'particulier/operations/moco/catitres/jcr:content.init.html', r'association/operations/moco/catitres/jcr:content.init.html', r'professionnel/operations/moco/catitres/jcr:content.init.html', r'agriculteur/operations/moco/catitres/jcr:content.init.html', r'entreprise/operations/moco/catitres/jcr:content.init.html', r'particulier/operations/moco/catitres/_jcr_content.init.html', r'association/operations/moco/catitres/_jcr_content.init.html', r'professionnel/operations/moco/catitres/_jcr_content.init.html', r'agriculteur/operations/moco/catitres/_jcr_content.init.html', r'entreprise/operations/moco/catitres/_jcr_content.init.html', NetfincaRedirectionPage) predica_redirection = URL(r'particulier/operations/moco/predica/jcr:content.init.html', r'association/operations/moco/predica/jcr:content.init.html', r'professionnel/operations/moco/predica/jcr:content.init.html', r'agriculteur/operations/moco/predica/jcr:content.init.html', r'entreprise/operations/moco/predica/jcr:content.init.html', PredicaRedirectionPage) predica_investments = URL(r'https://npcprediweb.predica.credit-agricole.fr/rest/detailEpargne/contrat/', PredicaInvestmentsPage) profile_page = URL(r'particulier/operations/synthese/jcr:content.npc.store.client.json', r'association/operations/synthese/jcr:content.npc.store.client.json', r'professionnel/operations/synthese/jcr:content.npc.store.client.json', r'agriculteur/operations/synthese/jcr:content.npc.store.client.json', r'entreprise/operations/synthese/jcr:content.npc.store.client.json', ProfilePage) profile_details = URL(r'particulier/operations/profil/infos-personnelles/gerer-coordonnees.html', ProfileDetailsPage) pro_profile_details = URL(r'association/operations/profil/infos-personnelles/controler-coordonnees.html', r'professionnel/operations/profil/infos-personnelles/controler-coordonnees.html', r'agriculteur/operations/profil/infos-personnelles/controler-coordonnees.html', r'entreprise/operations/profil/infos-personnelles/controler-coordonnees.html', ProProfileDetailsPage) recipients = URL('(?P.*)/operations/(?P.*)/virement/jcr:content.accounts.json', RecipientsPage) transfer_token = URL('(?P.*)/operations/(?P.*)/virement.npcgeneratetoken.json\?tokenTypeId=1', TransferTokenPage) transfer = URL('(?P.*)/operations/(?P.*)/virement/jcr:content.check-transfer.json', TransferPage) transfer_recap = URL('(?P.*)/operations/(?P.*)/virement/jcr:content.transfer-data.json\?useSession=true', TransferPage) transfer_exec = URL('(?P.*)/operations/(?P.*)/virement/jcr:content.process-transfer.json', TransferPage) def __init__(self, website, *args, **kwargs): super(CragrAPI, self).__init__(*args, **kwargs) website = website.replace('.fr', '') self.region = re.sub('^m\.', 'www.credit-agricole.fr/', website) self.BASEURL = 'https://%s/' % self.region self.accounts_url = None # Netfinca browser: self.weboob = kwargs.pop('weboob') dirname = self.responses_dirname self.netfinca = NetfincaBrowser('', '', logger=self.logger, weboob=self.weboob, responses_dirname=dirname, proxy=self.PROXIES) def deinit(self): super(CragrAPI, self).deinit() self.netfinca.deinit() def do_login(self): form = self.get_security_form() try: self.security_check.go(data=form) except ServerError as exc: # Wrongpass returns a 500 server error... error = exc.response.json().get('error') if error: message = error.get('message', '') wrongpass_messages = ("Votre identification est incorrecte", "Vous n'avez plus droit") if any(value in message for value in wrongpass_messages): raise BrowserIncorrectPassword() if 'obtenir un nouveau code' in message: raise ActionNeeded(message) technical_errors = ('Un incident technique', 'identifiant et votre code personnel') if any(value in message for value in technical_errors): # If it is a technical error, we try login again form = self.get_security_form() 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) # accounts_url may contain '/particulier', '/professionnel', '/entreprise', '/agriculteur' or '/association' self.accounts_url = self.page.get_accounts_url() assert self.accounts_url, 'Could not get accounts url from security check' self.location(self.accounts_url) assert self.accounts_page.is_here(), 'We failed to login after the security check: response URL is %s' % self.url # Once the security check is passed, we are logged in. def get_security_form(self): self.keypad.go() keypad_password = self.page.build_password(self.password[:6]) keypad_id = self.page.get_keypad_id() assert keypad_password, 'Could not obtain keypad password' assert keypad_id, 'Could not obtain keypad id' self.login_page.go() # Get the form data to POST the security check: form = self.page.get_login_form(self.username, keypad_password, keypad_id) return form @need_login def check_space_connection(self, contract): # Going to a specific space often returns a 500 error # so we might have to retry several times. try: self.go_to_account_space(contract) except ServerError: self.logger.warning('Server returned error 500 when trying to access space %s, we try again' % contract) try: self.go_to_account_space(contract) except ServerError: return False return True @need_login def get_accounts_list(self): # Determine how many spaces are present on the connection: self.location(self.accounts_url) if not self.accounts_page.is_here(): # We have been logged out. self.do_login() 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 = {} deferred_cards = {} for contract in range(total_spaces): if not self.check_space_connection(contract): self.logger.warning('Server returned error 500 twice when trying to access space %s, this space will be skipped' % contract) continue # The main account is not located at the same place in the JSON. main_account = self.page.get_main_account() if main_account.balance == NotAvailable: self.check_space_connection(contract) main_account = self.page.get_main_account() if main_account.balance == NotAvailable: self.logger.warning('Could not fetch the balance for main account %s.' % main_account.id) main_account.owner_type = self.page.get_owner_type() main_account._contract = contract accounts_list = list(self.page.iter_accounts()) for account in accounts_list: account._contract = contract account.owner_type = self.page.get_owner_type() ''' Other accounts have no balance in the main JSON, so we must get all the (_id_element_contrat, balance) pairs in the account_details JSON. Account categories always correspond to the same account types: # Category 1: Checking accounts, # Category 2: To be determined, # Category 3: Savings, # Category 4: Loans & Credits, # Category 5: Insurances (skipped), # Category 6: To be determined, # Category 7: Market accounts. ''' categories = {int(account._category) for account in accounts_list if account._category not in (None, '5')} account_balances = {} loan_ids = {} for category in categories: self.account_details.go(category=category) account_balances.update(self.page.get_account_balances()) loan_ids.update(self.page.get_loan_ids()) if main_account.type == Account.TYPE_CHECKING: params = { 'compteIdx': int(main_account._index), 'grandeFamilleCode': 1, } self.account_iban.go(params=params) iban = self.page.get_iban() if is_iban_valid(iban): main_account.iban = iban 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_element_contrat, NotAvailable) if account.type == Account.TYPE_CHECKING: 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 account.type == Account.TYPE_LOAN: 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 = 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 # Once again, this request tends to crash often. try: self.cards.go() except ClientError: self.logger.warning('Request to cards failed, we try again') try: self.check_space_connection(contract) self.cards.go() except ClientError: self.logger.warning('Request to cards failed twice, cards of this space will be skipped.') if self.cards.is_here(): 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 deferred_cards: deferred_cards[card.id] = card # We must check if cards are unique on their parent account; # if not, we cannot retrieve their summaries in iter_history. parent_accounts = [] for card in deferred_cards.values(): parent_accounts.append(card.parent.id) for card in deferred_cards.values(): if parent_accounts.count(card.parent.id) == 1: card._unique = True else: card._unique = False 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): self.contracts_page.go(id_contract=contract) if not self.accounts_page.is_here(): # We have been logged out. self.do_login() self.contracts_page.go(id_contract=contract) assert self.accounts_page.is_here() @need_login def get_history(self, account, coming=False): if account.type == Account.TYPE_CARD: card_transactions = [] self.go_to_account_space(account._contract) # Deferred cards transactions have a specific JSON. # Only three months of history available for cards. value = 0 if coming else 1 params = { 'grandeFamilleCode': int(account._category), 'compteIdx': int(account.parent._index), 'carteIdx': int(account._index), 'rechercheEncoursDebite': value } self.card_history.go(params=params) for tr in self.page.iter_card_history(): card_transactions.append(tr) # If the card if not unique on the parent id, it is impossible # to know which summary corresponds to which card. if not coming and card_transactions and account._unique: # Get card summaries from parent account # until we reach the oldest card transaction last_transaction = card_transactions[-1] before_last_transaction = False params = { 'compteIdx': int(account.parent._index), 'grandeFamilleCode': int(account.parent._category), 'idDevise': str(account.parent.currency), 'idElementContrat': str(account.parent._id_element_contrat), } self.history.go(params=params) for tr in self.page.iter_history(): if tr.date < last_transaction.date: before_last_transaction = True break if tr.type == Transaction.TYPE_CARD_SUMMARY: tr.amount = -tr.amount card_transactions.append(tr) while self.page.has_next_page() and not before_last_transaction: next_index = self.page.get_next_index() params = { 'grandeFamilleCode': int(account.parent._category), 'compteIdx': int(account.parent._index), 'idDevise': str(account.parent.currency), 'startIndex': next_index, 'count': 100, } self.history.go(params=params) for tr in self.page.iter_history(): if tr.date < last_transaction.date: before_last_transaction = True break if tr.type == Transaction.TYPE_CARD_SUMMARY: tr.amount = -tr.amount card_transactions.append(tr) for tr in sorted_transactions(card_transactions): yield tr return # These three parameters are required to get the transactions for non_card accounts if empty(account._index) or empty(account._category) or empty(account._id_element_contrat): return self.go_to_account_space(account._contract) params = { 'compteIdx': int(account._index), 'grandeFamilleCode': int(account._category), 'idDevise': str(account.currency), 'idElementContrat': str(account._id_element_contrat), } self.history.go(params=params) for tr in self.page.iter_history(): # For "Livret A", value dates of transactions are always # 1st or 15th of the month so we specify a valuation date. # Example: rdate = 21/02, date=01/02 then vdate = 01/02. if account.type == Account.TYPE_SAVINGS: tr.vdate = tr.date yield tr # Get other transactions 100 by 100: while self.page.has_next_page(): next_index = self.page.get_next_index() params = { 'grandeFamilleCode': int(account._category), 'compteIdx': int(account._index), 'idDevise': str(account.currency), 'startIndex': next_index, 'count': 100, } self.history.go(params=params) for tr in self.page.iter_history(): yield tr @need_login def iter_investment(self, account): if account.type in (Account.TYPE_PERP, Account.TYPE_PERCO, Account.TYPE_LIFE_INSURANCE, Account.TYPE_CAPITALISATION): if account.label == "Vers l'avenir": # Website crashes when clicking on these Life Insurances... return self.go_to_account_space(account._contract) token = self.token_page.go().get_token() data = { 'situation_travail': 'CONTRAT', 'idelco': account.id, ':cq_csrf_token': token, } self.predica_redirection.go(data=data) self.predica_investments.go() for inv in self.page.iter_investments(): yield inv elif account.type == Account.TYPE_PEA and account.label == 'Compte espèce PEA': yield create_french_liquidity(account.balance) return elif account.type in (Account.TYPE_PEA, Account.TYPE_MARKET): # Do not try to get to Netfinca if there is no money # on the account or the server will return an error 500 if account.balance == 0: return self.go_to_account_space(account._contract) token = self.token_page.go().get_token() data = { 'situation_travail': 'BANCAIRE', 'num_compte': account.id, 'code_fam_produit': account._fam_product_code, 'code_fam_contrat_compte': account._fam_contract_code, ':cq_csrf_token': token, } # For some market accounts, investments are not even accessible, # and the only way to know if there are investments is to try # to go to the Netfinca space with the accounts parameters. try: self.netfinca_redirection.go(data=data) except BrowserHTTPNotFound: self.logger.info('Investments are not available for this account.') self.go_to_account_space(account._contract) return url = self.page.get_url() if 'netfinca' in url: self.location(url) self.netfinca.session.cookies.update(self.session.cookies) self.netfinca.accounts.go() for inv in self.netfinca.iter_investments(account): if inv.code == 'XX-liquidity' and account.type == Account.TYPE_PEA: # Liquidities are already fetched on the "PEA espèces" continue yield inv @need_login def iter_advisor(self): self.go_to_account_space(0) owner_type = self.page.get_owner_type() self.profile_page.go() if owner_type == 'PRIV': advisor = self.page.get_advisor() self.profile_details.go() self.page.fill_advisor(obj=advisor) yield advisor elif owner_type == 'ORGA': advisor = self.page.get_advisor() self.pro_profile_details.go() self.page.fill_advisor(obj=advisor) yield advisor @need_login def get_profile(self): # There is one profile per space, so we only fetch the first one self.go_to_account_space(0) owner_type = self.page.get_owner_type() self.profile_page.go() if owner_type == 'PRIV': profile = self.page.get_user_profile() self.profile_details.go() self.page.fill_profile(obj=profile) return profile elif owner_type == 'ORGA': profile = self.page.get_company_profile() self.pro_profile_details.go() self.page.fill_profile(obj=profile) return profile @need_login def get_account_transfer_space_info(self, account): self.go_to_account_space(account._contract) space = self.session.cookies['marche'] connection_id = self.page.get_connection_id() operations = { 'particulier': 'moyens-paiement', 'professionnel': 'paiements-encaissements', 'association': 'paiements-encaissements', 'entreprise': 'paiements-encaissements', } referer = self.absurl('/%s/operations/%s/virement.html.html' % (space, operations[space])) return space, operations[space], referer, connection_id @need_login def iter_debit_accounts(self): assert self.recipients.is_here() for index, debit_accounts in enumerate(self.page.iter_debit_accounts()): debit_accounts._index = index yield debit_accounts @need_login def iter_transfer_recipients(self, account, transfer_space_info=None): if account.type in (account.TYPE_CARD, account.TYPE_LOAN, account.TYPE_LIFE_INSURANCE, account.TYPE_PEA, account.TYPE_CONSUMER_CREDIT, account.TYPE_REVOLVING_CREDIT, ): return # avoid to call `get_account_transfer_space_info()` several time if transfer_space_info: space, operation, referer = transfer_space_info else: space, operation, referer, _ = self.get_account_transfer_space_info(account) self.recipients.go(space=space, op=operation, headers={'Referer': referer}) if not self.page.is_sender_account(account.id): return # can't use 'ignore_duplicate' in DictElement because we need the 'index' to do transfer seen = set() seen.add(account.iban) for index, internal_rcpt in enumerate(self.page.iter_internal_recipient()): internal_rcpt._index = index if internal_rcpt._is_recipient and (internal_rcpt.iban not in seen): seen.add(internal_rcpt.iban) yield internal_rcpt for index, external_rcpt in enumerate(self.page.iter_external_recipient()): external_rcpt._index = index if external_rcpt.iban not in seen: seen.add(external_rcpt.iban) yield external_rcpt @need_login def init_transfer(self, transfer, **params): # first, get _account on account list to get recipient _account = strict_find_object(self.get_accounts_list(), id=transfer.account_id, error=AccountNotFound) # get information to go on transfer page space, operation, referer, connection_id = self.get_account_transfer_space_info(account=_account) recipient = strict_find_object( self.iter_transfer_recipients(_account, transfer_space_info=(space, operation, referer)), id=transfer.recipient_id, error=RecipientNotFound ) # Then, get account on transfer account list to get index and other information account = strict_find_object(self.iter_debit_accounts(), id=_account.id, error=AccountNotFound) # get token and transfer token to init transfer token = self.token_page.go().get_token() transfer_token = self.transfer_token.go(space=space, op=operation, headers={'Referer': referer}).get_token() data = { 'connexionId': connection_id, 'cr': self.session.cookies['caisse-regionale'], 'creditAccountIban': recipient.iban, 'creditAccountIndex': recipient._index, 'debitAccountIndex': account._index, 'debitAccountNumber': account.number, 'externalAccount': recipient.category == 'Externe', 'recipientName': recipient.label, 'transferAmount': transfer.amount, 'transferComplementaryInformation1': transfer.label, 'transferComplementaryInformation2': '', 'transferComplementaryInformation3': '', 'transferComplementaryInformation4': '', 'transferCurrencyCode': account.currency, 'transferDate': transfer.exec_date.strftime('%d/%m/%Y'), 'transferFrequency': 'U', 'transferRef': '', 'transferType': 'UNIQUE', 'typeCompte': account.label, } # init transfer request self.transfer.go( space=space, op=operation, headers={'Referer': referer, 'CSRF-Token': token, 'NPC-Generated-Token': transfer_token}, json=data ) assert self.page.check_transfer() # get recap because it's not returned by init transfer request self.transfer_recap.go( space=space, op=operation, headers={'Referer': self.absurl('/%s/operations/%s/virement.postredirect.html' % (space, operation))} ) # information needed to exec transfer transfer._space = space transfer._operation = operation transfer._token = token transfer._connection_id = connection_id return self.page.handle_response(transfer) @need_login def execute_transfer(self, transfer, **params): self.transfer_exec.go( space=transfer._space, op=transfer._operation, headers={ 'Referer': self.absurl('/%s/operations/%s/virement.postredirect.html' % (transfer._space, transfer._operation)), 'CSRF-Token': transfer._token }, json={'connexionId': transfer._connection_id} ) assert self.page.check_transfer_exec() return transfer @need_login def build_recipient(self, recipient): raise BrowserUnavailable() @need_login def new_recipient(self, recipient, **params): raise BrowserUnavailable()