diff --git a/modules/bnpcards/browser.py b/modules/bnpcards/browser.py
index a10f33ca5d1587b8ac62b949fbc76f7c58157ea5..8f5b5a8028726d54c30a65a01d20185b538cc546 100644
--- a/modules/bnpcards/browser.py
+++ b/modules/bnpcards/browser.py
@@ -73,6 +73,8 @@ def __init__(self, type, *args, **kwargs):
def do_login(self):
assert isinstance(self.username, basestring)
assert isinstance(self.password, basestring)
+ if self.type == '1':
+ raise SiteSwitch('phenix')
self.login.stay_or_go()
assert self.login.is_here()
self.page.login(self.type, self.username, self.password)
diff --git a/modules/bnpcards/phenix/__init__.py b/modules/bnpcards/phenix/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/modules/bnpcards/phenix/browser.py b/modules/bnpcards/phenix/browser.py
new file mode 100644
index 0000000000000000000000000000000000000000..43b79004d9aef80543ef9a8585aef0bfaa4a7be2
--- /dev/null
+++ b/modules/bnpcards/phenix/browser.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2019 Budget Insight
+#
+# 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 Lesser 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this weboob module. If not, see .
+
+
+from weboob.exceptions import BrowserIncorrectPassword, BrowserPasswordExpired
+from weboob.browser import LoginBrowser, URL, need_login
+
+from .pages import (
+ LoginPage, DashboardPage, TransactionPage, TransactionCSV,
+ PasswordExpiredPage,
+)
+
+__all__ = ['BnpcartesentreprisePhenixBrowser']
+
+
+class BnpcartesentreprisePhenixBrowser(LoginBrowser):
+ BASEURL = 'https://corporatecards.bnpparibas.com'
+
+ login_cas = URL(r'https://cartesentreprise-oidc.phenix.bnpparibas/login', LoginPage)
+ login = URL(r'/login', LoginPage)
+ dashboard = URL(r'/group/bddf/dashboard', DashboardPage)
+ transaction_csv = URL(
+ r'/group/bddf/transactions\?p_p_id=Phenix_Transactions_Portlet_INSTANCE_(?P.*)'
+ r'&p_p_lifecycle=2&p_p_state=normal&p_p_mode=view&p_p_resource_id=/transaction/export&p_p_cacheability=cacheLevelPage&'
+ r'_Phenix_Transactions_Portlet_INSTANCE_(?P.*)_MVCResourceCommand=/transaction/export',
+ TransactionCSV
+ )
+ transactions_page = URL(r'/group/bddf/transactions', TransactionPage)
+ password_expired = URL(r'https://corporatecards.bnpparibas.com/group/bddf/mot-de-passe-expire', PasswordExpiredPage)
+
+ def __init__(self, website, *args, **kwargs):
+ super(BnpcartesentreprisePhenixBrowser, self).__init__(*args, **kwargs)
+ self.website = website
+
+ def do_login(self):
+ self.login_cas.go()
+ self.page.login(self.username, self.password)
+ if not(self.page.is_logged()):
+ raise BrowserIncorrectPassword(self.page.get_error_message())
+ self.dashboard.go()
+ if self.password_expired.is_here():
+ raise BrowserPasswordExpired(self.page.get_error_message())
+
+ @need_login
+ def iter_accounts(self):
+ self.dashboard.go()
+ for account in self.page.iter_accounts():
+ self.location(account.url)
+ yield self.page.fill_account(obj=account)
+
+ @need_login
+ def get_transactions(self, account):
+ self.dashboard.stay_or_go()
+ self.location(account.url)
+ self.transactions_page.go()
+ instance_id = self.page.get_instance_id()
+ page_csv = self.transaction_csv.open(method="POST", instance_id1=instance_id, instance_id2=instance_id)
+ for tr in page_csv.iter_history():
+ yield tr
diff --git a/modules/bnpcards/phenix/pages.py b/modules/bnpcards/phenix/pages.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f8759aab005fe2d81f43a61d20567e3840c9669
--- /dev/null
+++ b/modules/bnpcards/phenix/pages.py
@@ -0,0 +1,166 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2019 Budget Insight
+#
+# 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 Lesser 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this weboob module. If not, see .
+
+from __future__ import unicode_literals
+
+import sys
+from datetime import date
+
+from weboob.browser.filters.standard import (
+ CleanText, CleanDecimal, Date, MapIn, Field,
+ Currency, Regexp, Format, Eval,
+)
+from weboob.browser.filters.json import Dict
+from weboob.browser.filters.html import Attr, Link
+from weboob.capabilities.bank import Account, Transaction
+from weboob.browser.elements import (
+ DictElement, ListElement, ItemElement, method,
+)
+from weboob.capabilities.base import NotAvailable
+from weboob.tools.compat import unicode
+from weboob.browser.pages import HTMLPage, LoggedPage, CsvPage
+
+
+class LoginPage(HTMLPage):
+ def login(self, username, password):
+ form = self.get_form(id='fm1')
+ form['username'] = username
+ form['password'] = password
+ form.submit()
+
+ def get_error_message(self):
+ return CleanText('//div[@class="alert alert-danger"]')(self.doc)
+
+ def is_logged(self):
+ return CleanText('//div[@id="successTextBloc"]/h2')(self.doc) == 'Log In Successful'
+
+
+class DashboardPage(LoggedPage, HTMLPage):
+ @method
+ class iter_accounts(ListElement):
+ item_xpath = '//div[@class="container header_desktop"]//a[@class="carte"]'
+
+ class item(ItemElement):
+ klass = Account
+
+ def condition(self):
+ return CleanText('./span[@class="lanNavSel_succes_carte"]')(self)
+
+ obj_url = Link('.')
+
+ @method
+ class fill_account(ItemElement):
+ obj_type = Account.TYPE_CARD
+ # most xpaths in the module are long and strict because the html contains multiple versions of
+ # the site that are displayed or not via js depending on the size of the screen
+ # they are not displayed but still present, so the xpaths can catch them
+ # and we want to avoid that
+ obj_number = CleanText('//div[@class="row bnp_carte_dashboard_one"]//span[@id="carte_dashboard_numero_compte"]')
+ obj_id = Field('number')
+
+ obj_label = Format(
+ '%s %s',
+ CleanText('//div[@class="row bnp_carte_dashboard_one"]//span[@id="carte_dashboard_title"]'),
+ CleanText('//div[contains(@class,"hidden-xs")]//div[contains(@class,"bnp_info_general_one")]//ul[@class="list-group bnp_information"]/li[2]')
+ )
+
+ # TODO Handle 'Fin du mois'
+ obj_paydate = Date(
+ Regexp(
+ CleanText('//div[@class="row prelevement"]/div[@class="prelevement-box"][1]/span[@class="prelevement_le"]'),
+ r'(\d{2}/\d{2}/\d{4})',
+ default=NotAvailable
+ ),
+ dayfirst=True,
+ default=NotAvailable
+ )
+
+ obj_currency = Currency(
+ CleanText('//div[@class="plafondStyle"]//p[@class="paiement_content_color"]')
+ )
+
+ obj_coming = Eval(
+ lambda x, y: x + y,
+ CleanDecimal.French('//div[contains(@class, "hidden-xs")]//div[contains(@class, "cumul_content_dashboard")]//span[@class = "content_paiement"]', default=NotAvailable),
+ CleanDecimal.French('//div[contains(@class, "hidden-xs")]//div[contains(@class, "cumul_content_dashboard")]//span[@class = "content_retrait"]', default=NotAvailable)
+ )
+
+
+class TransactionPage(LoggedPage, HTMLPage):
+ def get_instance_id(self):
+ return Regexp(
+ Attr('//span[contains(@id,"p_Phenix_Transactions_Portlet_INSTANCE_")]', 'id'),
+ r'INSTANCE_([^_]*)'
+ )(self.doc)
+
+
+class TransactionCSV(LoggedPage, CsvPage):
+ HEADER = 9
+
+ FMTPARAMS = {'delimiter': ';'}
+
+ def build_doc(self, content):
+ # Dict splits keys on '/' it is intended behaviour because it's primary
+ # use is with json files, but it means I have to replace '/' here
+ delimiter = self.FMTPARAMS.get('delimiter')
+ if sys.version_info.major == 2 and delimiter and isinstance(delimiter, unicode):
+ self.FMTPARAMS['delimiter'] = delimiter.encode('utf-8')
+ content = content.replace(b'/', b'-')
+ return super(TransactionCSV, self).build_doc(content)
+
+ @method
+ class iter_history(DictElement):
+ class item(ItemElement):
+ klass = Transaction
+
+ TRANSACTION_TYPES = {
+ 'FACTURE CB': Transaction.TYPE_CARD,
+ 'RETRAIT CB': Transaction.TYPE_WITHDRAWAL,
+ }
+
+ obj_label = CleanText(Dict('Raison sociale commerçant'))
+ obj_amount = CleanDecimal.French(Dict('Montant EUR'))
+ obj_original_currency = CleanText(Dict("Devise d'origine"))
+ obj_rdate = Date(CleanText(Dict("Date d'opération")), dayfirst=True)
+ obj_date = Date(CleanText(Dict('Date débit-crédit')), dayfirst=True)
+
+ obj_type = MapIn(
+ CleanText(Dict('Libellé opération')),
+ TRANSACTION_TYPES
+ )
+
+ def obj_commission(self):
+ commission = CleanDecimal.French(Dict('Commission'))(self)
+ if commission != 0: # We don't want to return null commissions
+ return commission
+ return NotAvailable
+
+ def obj_original_amount(self):
+ original_amount = CleanDecimal.French(Dict("Montant d'origine"))(self)
+ if original_amount != 0: # We don't want to return null original_amounts
+ return original_amount
+ return NotAvailable
+
+ def obj__coming(self):
+ return Field('date')(self) >= date.today()
+
+
+class PasswordExpiredPage(LoggedPage, HTMLPage):
+ def get_error_message(self):
+ return CleanText('//span[@class="messageWarning"]')(self.doc)
diff --git a/modules/bnpcards/proxy_browser.py b/modules/bnpcards/proxy_browser.py
index 66e4b0b430ffb345a01fbd843730bb6e279620d5..a6432998548aab9b5492bc6542d95abfe5a419e2 100644
--- a/modules/bnpcards/proxy_browser.py
+++ b/modules/bnpcards/proxy_browser.py
@@ -22,10 +22,12 @@
from .browser import BnpcartesentrepriseBrowser
from .corporate.browser import BnpcartesentrepriseCorporateBrowser
+from .phenix.browser import BnpcartesentreprisePhenixBrowser
class ProxyBrowser(SwitchingBrowser):
BROWSERS = {
'main': BnpcartesentrepriseBrowser,
'corporate': BnpcartesentrepriseCorporateBrowser,
+ 'phenix': BnpcartesentreprisePhenixBrowser,
}