transfer.py 12.9 KB
Newer Older
Baptiste Delpey's avatar
Baptiste Delpey committed
1 2 3 4
# -*- coding: utf-8 -*-

# Copyright(C) 2016 Baptiste Delpey
#
5
# This file is part of a weboob module.
Baptiste Delpey's avatar
Baptiste Delpey committed
6
#
7
# This weboob module is free software: you can redistribute it and/or modify
Romain Bignon's avatar
Romain Bignon committed
8
# it under the terms of the GNU Lesser General Public License as published by
Baptiste Delpey's avatar
Baptiste Delpey committed
9 10 11
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
12
# This weboob module is distributed in the hope that it will be useful,
Baptiste Delpey's avatar
Baptiste Delpey committed
13 14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Romain Bignon's avatar
Romain Bignon committed
15
# GNU Lesser General Public License for more details.
Baptiste Delpey's avatar
Baptiste Delpey committed
16
#
Romain Bignon's avatar
Romain Bignon committed
17
# You should have received a copy of the GNU Lesser General Public License
18
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
Baptiste Delpey's avatar
Baptiste Delpey committed
19

20 21
# flake8: compatible

22 23
from __future__ import unicode_literals

24
from datetime import datetime
Baptiste Delpey's avatar
Baptiste Delpey committed
25 26
import re

27
from weboob.browser.pages import LoggedPage, JsonPage, FormNotFound
28
from weboob.browser.elements import method, ItemElement, DictElement
29
from weboob.capabilities.bank import (
30
    Recipient, Transfer, TransferBankError, AddRecipientBankError, AddRecipientTimeout,
31
    Emitter, EmitterNumberType,
32
)
33
from weboob.tools.capabilities.bank.iban import is_iban_valid
34 35 36 37
from weboob.capabilities.base import NotAvailable
from weboob.browser.filters.standard import (
    CleanText, CleanDecimal, Env, Date, Field, Format,
)
38
from weboob.browser.filters.html import Link, ReplaceEntities
39
from weboob.browser.filters.json import Dict
40
from weboob.tools.json import json
41
from weboob.exceptions import BrowserUnavailable, ActionNeeded
Baptiste Delpey's avatar
Baptiste Delpey committed
42 43

from .base import BasePage
44
from .login import MainPage
45
from .accounts_list import eval_decimal_amount
Baptiste Delpey's avatar
Baptiste Delpey committed
46 47


48
class TransferJson(LoggedPage, JsonPage):
49 50 51 52
    @property
    def logged(self):
        return Dict('commun/raison', default=None)(self.doc) != "niv_auth_insuff"

53 54 55
    def on_load(self):
        if Dict('commun/statut')(self.doc).upper() == 'NOK':
            if self.doc['commun'].get('action'):
56
                raise TransferBankError(message=ReplaceEntities(Dict('commun/action'))(self.doc))
57
            elif self.doc['commun'].get('raison') in ('err_tech', 'err_is'):
58 59
                # on SG website, there is unavalaible message 'Le service est momentanément indisponible.'
                raise BrowserUnavailable()
60 61
            elif self.doc['commun'].get('raison') == "niv_auth_insuff":
                return
62
            else:
63 64 65 66
                raise AssertionError(
                    'Something went wrong, transfer is not created: %s'
                    % self.doc['commun'].get('raison')
                )
Baptiste Delpey's avatar
Baptiste Delpey committed
67

68 69
    def get_acc_transfer_id(self, account):
        for acc in self.doc['donnees']['listeEmetteursBeneficiaires']['listeDetailEmetteurs']:
70 71 72 73
            if (
                account.id == Format('%s%s', Dict('codeGuichet'), Dict('numeroCompte'))(acc)
                or account.id == Dict('identifiantPrestation', default=NotAvailable)(acc)
            ):
74 75 76
                # return json_id to do transfer
                return acc['id']
        return False
Baptiste Delpey's avatar
Baptiste Delpey committed
77

78 79
    def is_able_to_transfer(self, account):
        return self.get_acc_transfer_id(account)
Baptiste Delpey's avatar
Baptiste Delpey committed
80

81
    def get_first_available_transfer_date(self):
82
        return Date(Dict('donnees/listeEmetteursBeneficiaires/premiereDateExecutionPossible'), dayfirst=True)(self.doc)
83

84 85 86 87 88 89
    def get_account_ibans_dict(self):
        account_ibans = {}
        for account in Dict('donnees/listeEmetteursBeneficiaires/listeDetailEmetteurs')(self.doc):
            account_ibans[Dict('identifiantPrestation')(account)] = Dict('iban')(account)
        return account_ibans

Baptiste Delpey's avatar
Baptiste Delpey committed
90
    @method
91 92
    class iter_recipients(DictElement):
        item_xpath = 'donnees/listeEmetteursBeneficiaires/listeDetailBeneficiaires'
93 94
        # Some recipients can be internal and external
        ignore_duplicate = True
Baptiste Delpey's avatar
Baptiste Delpey committed
95

96 97
        class Item(ItemElement):
            klass = Recipient
Baptiste Delpey's avatar
Baptiste Delpey committed
98

99 100 101 102 103
            # Assume all recipients currency is euros.
            obj_currency = u'EUR'
            obj_iban = Dict('iban')
            obj_label = Dict('libelleToDisplay')
            obj_enabled_at = datetime.now().replace(microsecond=0)
Baptiste Delpey's avatar
Baptiste Delpey committed
104

105 106
            # needed for transfer
            obj__json_id = Dict('id')
107

108 109 110 111
            def obj_category(self):
                if Dict('groupeRoleToDisplay')(self) == 'Comptes personnels':
                    return u'Interne'
                return u'Externe'
Baptiste Delpey's avatar
Baptiste Delpey committed
112

113 114 115 116 117
            # for retrocompatibility
            def obj_id(self):
                if Field('category')(self) == 'Interne':
                    return Format('%s%s', Dict('codeGuichet'), Dict('numeroCompte'))(self)
                return Dict('iban')(self)
118

119
            def condition(self):
120
                return Field('id')(self) != Env('account_id')(self) and is_iban_valid(Field('iban')(self))
121

122 123 124
            def validate(self, obj):
                return obj.label  # some recipients have an empty label

125 126 127 128 129 130 131 132 133 134 135 136 137
    def init_transfer(self, account, recipient, transfer):
        assert self.is_able_to_transfer(account), 'Account %s seems to be not able to do transfer' % account.id

        # SCT : standard transfer
        data = [
            ('an200_montant', transfer.amount),
            ('an200_typeVirement', 'SCT'),
            ('b64e200_idCompteBeneficiaire', recipient._json_id),
            ('b64e200_idCompteEmetteur', self.get_acc_transfer_id(account)),
            ('cl200_devise', u'EUR'),
            ('cl200_nomBeneficiaire', recipient.label),
            ('cl500_motif', transfer.label),
            ('dt10_dateExecution', transfer.exec_date.strftime('%d/%m/%Y')),
138
        ]
Baptiste Delpey's avatar
Baptiste Delpey committed
139

140 141
        headers = {'Referer': self.browser.absurl('/com/icd-web/vupri/virement.html')}
        self.browser.location(self.browser.absurl('/icd/vupri/data/vupri-check.json'), headers=headers, data=data)
Baptiste Delpey's avatar
Baptiste Delpey committed
142

143 144
    def handle_response(self, recipient):
        json_response = self.doc['donnees']
145

146 147 148 149 150
        transfer = Transfer()
        transfer.id = json_response['idVirement']
        transfer.label = json_response['motif']
        transfer.amount = CleanDecimal.French((CleanText(Dict('montantToDisplay'))))(json_response)
        transfer.currency = json_response['devise']
151
        transfer.exec_date = Date(Dict('dateExecution'), dayfirst=True)(json_response)
152

153 154 155
        transfer.account_id = Format('%s%s', Dict('codeGuichet'), Dict('numeroCompte'))(json_response['compteEmetteur'])
        transfer.account_iban = json_response['compteEmetteur']['iban']
        transfer.account_label = json_response['compteEmetteur']['libelleToDisplay']
156

157 158 159 160
        assert recipient._json_id == json_response['compteBeneficiaire']['id']
        transfer.recipient_id = recipient.id
        transfer.recipient_iban = json_response['compteBeneficiaire']['iban']
        transfer.recipient_label = json_response['compteBeneficiaire']['libelleToDisplay']
161

162
        return transfer
163

164 165
    def is_transfer_validated(self):
        return Dict('commun/statut')(self.doc).upper() == 'OK'
Baptiste Delpey's avatar
Baptiste Delpey committed
166

167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
    @method
    class iter_emitters(DictElement):
        item_xpath = 'donnees/listeEmetteursBeneficiaires/listeDetailEmetteurs'

        class Item(ItemElement):
            klass = Emitter

            obj_id = Dict('numeroCompte')
            obj_label = Dict('libelleToDisplay')
            obj_currency = Dict('montantSoldeVeille/codeDevise')
            obj_balance = eval_decimal_amount(
                'montantSoldeVeille/valeurMontant',
                'montantSoldeVeille/codeDecimalisation'
            )
            obj_number_type = EmitterNumberType.IBAN
            obj_number = Dict('iban')

Baptiste Delpey's avatar
Baptiste Delpey committed
184

185
class SignTransferPage(LoggedPage, MainPage):
186 187 188 189
    def get_token(self):
        result_page = json.loads(self.content)
        assert result_page['commun']['statut'].upper() == 'OK', 'Something went wrong: %s' % result_page['commun']['raison']
        return result_page['donnees']['jeton']
190

191 192
    def get_confirm_transfer_data(self, password):
        token = self.get_token()
193
        keyboard_data = self.get_keyboard_data()
Baptiste Delpey's avatar
Baptiste Delpey committed
194

195
        pwd = keyboard_data['img'].get_codes(password[:6])
Baptiste Delpey's avatar
Baptiste Delpey committed
196
        t = pwd.split(',')
197
        newpwd = ','.join(t[self.strange_map[j]] for j in range(6))
198 199 200

        return {
            'codsec': newpwd,
201
            'cryptocvcs': keyboard_data['infos']['crypto'].encode('iso-8859-1'),
202 203 204
            'vkm_op': 'sign',
            'cl1000_jtn': token,
        }
205 206


207 208
class SignRecipientPage(LoggedPage, JsonPage):
    def on_load(self):
209
        assert Dict('commun/statut')(self.doc).upper() == 'OK', (
210
            'Something went wrong on sign recipient page: %s' % Dict('commun/raison')(self.doc)
211
        )
212 213

    def get_sign_method(self):
214
        if Dict('donnees/unavailibility_reason', default='')(self.doc) == 'oob_non_enrole':
215 216
            # message from the website
            raise AddRecipientBankError(message="Pour réaliser cette opération il est nécessaire d'utiliser le PASS SECURITE")
217 218 219 220 221 222
        return Dict('donnees/sign_proc')(self.doc).upper()

    def check_recipient_status(self):
        transaction_status = Dict('donnees/transaction_status')(self.doc)

        # check add new recipient status
223
        assert transaction_status in ('available', 'in_progress', 'aborted', 'rejected'), (
224
            'transaction_status is %s' % transaction_status
225
        )
226 227 228 229 230 231 232 233 234
        if transaction_status == 'aborted':
            raise AddRecipientTimeout()
        elif transaction_status == 'rejected':
            raise ActionNeeded("La demande d'ajout de bénéficiaire a été annulée.")
        elif transaction_status == 'in_progress':
            raise ActionNeeded('Veuillez valider le bénéficiaire sur votre application bancaire.')

    def get_transaction_id(self):
        return Dict('donnees/id-transaction')(self.doc)
235

236

237 238
class AddRecipientPage(LoggedPage, BasePage):
    def on_load(self):
239
        error_msg = CleanText('//span[@class="error_msg"]')(self.doc)
240
        if error_msg:
241 242 243 244 245
            if 'Le service est momentanément indisponible' in error_msg:
                # This has been seen on multiple connections. Whenever they tried
                # to add a recipient it failed with this message, but it worked
                # when they tried to do it the next day.
                raise BrowserUnavailable(error_msg)
246
            raise AddRecipientBankError(message=error_msg)
247 248

    def is_here(self):
249 250 251
        return (
            bool(CleanText('//h3[contains(text(), "Ajouter un compte bénéficiaire de virement")]')(self.doc))
            or bool(CleanText('//h1[contains(text(), "Ajouter un compte bénéficiaire de virement")]')(self.doc))
252 253 254
            or bool(
                CleanText('//h3[contains(text(), "Veuillez vérifier les informations du compte à ajouter")]')(self.doc)
            )
255 256 257
            or bool(CleanText('//span[contains(text(), "Le service est momentanément indisponible")]')(self.doc))
            or bool(Link('//a[contains(@href, "per_cptBen_ajouter")]', default=NotAvailable)(self.doc))
        )
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274

    def post_iban(self, recipient):
        form = self.get_form(name='persoAjoutCompteBeneficiaire')
        form['codeIBAN'] = recipient.iban
        form['n10_form_etr'] = '1'
        form.submit()

    def post_label(self, recipient):
        form = self.get_form(name='persoAjoutCompteBeneficiaire')
        form['nomBeneficiaire'] = recipient.label
        form['codeIBAN'] = form['codeIBAN'].replace(' ', '')
        form['n10_form_etr'] = '1'
        form.submit()

    def get_action_level(self):
        for script in self.doc.xpath('//script'):
            if 'actionLevel' in CleanText('.')(script):
275
                return re.search(r"'actionLevel': (\d{3}),", script.text).group(1)
276

277
    def get_signinfo_data_form(self):
278 279 280
        try:
            form = self.get_form(id='formCache')
        except FormNotFound:
281
            raise AssertionError('Transfer auth form not found')
282
        return form
283

284 285 286
    def update_browser_recipient_state(self):
        form = self.get_signinfo_data_form()
        # set browser variable used to continue new recipient
287 288 289 290
        self.browser.context = form['context']
        self.browser.dup = form['dup']
        self.browser.logged = 1

291 292 293 294 295 296
    def get_signinfo_data(self):
        form = self.get_signinfo_data_form()
        signinfo_data = {}
        signinfo_data['b64_jeton_transaction'] = form['context']
        signinfo_data['action_level'] = self.get_action_level()
        return signinfo_data
297 298 299

    def get_recipient_object(self, recipient, get_info=False):
        r = Recipient()
300

301
        if get_info:
302 303 304 305
            recap_iban = CleanText(
                '//div[div[contains(text(), "IBAN")]]/div[has-class("recapTextField")]',
                replace=[(' ', '')]
            )(self.doc)
306
            assert recap_iban == recipient.iban
307 308 309 310 311

            recipient.bank_name = CleanText(
                '//div[div[contains(text(), "Banque du")]]/div[has-class("recapTextField")]'
            )(self.doc)

312 313 314 315 316 317 318 319 320
        r.iban = recipient.iban
        r.id = recipient.iban
        r.label = recipient.label
        r.category = recipient.category
        # On societe generale recipients are immediatly available.
        r.enabled_at = datetime.now().replace(microsecond=0)
        r.currency = u'EUR'
        r.bank_name = recipient.bank_name
        return r