diff --git a/modules/ccf/browser.py b/modules/ccf/browser.py index 4d4b39b3d5bd7b0b726e83af2f45a2cd57355c58..0965d47492a2e8ce8ec5c69de364ae46014fa0d4 100644 --- a/modules/ccf/browser.py +++ b/modules/ccf/browser.py @@ -15,10 +15,15 @@ # You should have received a copy of the GNU Lesser General Public License # along with this woob module. If not, see . +from datetime import date +from dateutil.relativedelta import relativedelta + import random from base64 import b64encode from hashlib import sha256 +from schwifty import IBAN + from woob.browser import URL, need_login from woob.browser.browsers import ClientError, ServerError from woob_modules.cmso.par.browser import CmsoParBrowser @@ -27,7 +32,7 @@ from woob.tools.decorators import retry -from .pages import SubscriptionsPage, DocumentsPage, RibPage, TransactionsPage +from .pages import SubscriptionsPage, DocumentsPage, RibPage, TransactionsPage, AccountsPage __all__ = ["CCFParBrowser", "CCFProBrowser"] @@ -37,6 +42,23 @@ class CCFBrowser(CmsoParBrowser): arkea_si = None AUTH_CLIENT_ID = "S4dgkKwTA7FQzWxGRHPXe6xNvihEATOY" + # Use CmsoParBrowser as base, but rely on /distri-account-api/api + # for accounts list & balance. Like modules/allianzbanque/browser.py + # We should probably extract a common browser. + + + # accounts_: note the trailing underscore + # don't override super.accounts, used indirectly by get_ibans_from_ribs + accounts_ = URL( + r"/distri-account-api/api/v1/persons/me/accounts", AccountsPage + ) + balance = URL( + r"/distri-account-api/api/v1/customers/me/accounts/(?P.*)/balances", AccountsPage + ) + balances_comings = URL( + r'/distri-account-api/api/v1/persons/me/accounts/(?P[A-Z0-9]{10})/total-upcoming-transactions', + AccountsPage + ) subscriptions = URL( r"/distri-account-api/api/v1/customers/me/accounts", SubscriptionsPage ) @@ -104,17 +126,12 @@ def build_request(self, *args, **kwargs): @need_login def get_subscription_list(self): - accounts_list = self.iter_accounts() - subscriptions = [] - for account in accounts_list: - s = Subscription() - s.label = account._lib - if account.number: - s.label = f"{s.label} {account.number}" - s.subscriber = account._owner_name - s.id = account.id - subscriptions.append(s) - return subscriptions + params = { + 'types': 'CHECKING', + 'roles': 'TIT,COT', + } + self.subscriptions.go(params=params) + return self.page.iter_subscriptions() @need_login def iter_documents(self, subscription): @@ -126,18 +143,54 @@ def download_document(self, document): params = {"flattenDoc": False} return self.open(document.url, params=params).content - def update_iban(self, account): - self.rib_details.go(json={"numeroContratSouscritCrypte": account._index}) - iban_number = self.page.get_iban() - if not account.iban: - account.iban = iban_number + def _update_iban(self, account, ibans): + iban_obj = None + if account.id in ibans: + account.iban = ibans.get(account.id) + iban_obj = IBAN(account.iban) if account.iban else None + else: + _iban = account._iban_offended + _contract = account._contract_id + + if _iban: + iban_obj = IBAN.generate( + country_code=_iban[:2], + bank_code=_iban[4:9], + branch_code=_iban[9:14], + account_code=_contract[0]+_contract[4:14]) + + if iban_obj: + account.iban = iban_obj.formatted + account.number = iban_obj.account_code + account.bank_name = iban_obj.bank_name + if not account.bank_name: + account.bank_name = 'CCF' + + def _get_ibans_from_ribs(self): + accounts_list = super().iter_accounts() + for account in accounts_list: + self.rib_details.go(json={"numeroContratSouscritCrypte": account._index}) + iban_number = self.page.get_iban() + if not account.iban: + account.iban = iban_number + return { account.id: account.iban for account in accounts_list } + @need_login def iter_accounts(self): - accounts_list = super().iter_accounts() + ibans = self._get_ibans_from_ribs() + + go_accounts = retry(ClientError, tries=5)(self.accounts_.go) + go_accounts(params={'types': 'CHECKING,SAVING'}) + + accounts_list = list(self.page.iter_accounts()) for account in accounts_list: - account._original_id = account.id - self.update_iban(account) - return accounts_list + self.balance.go(account_id=account.id) + self.page.fill_balance(account) + date_to = (date.today() + relativedelta(months=1)).strftime('%Y-%m-%dT00:00:00.000Z') + self.balances_comings.go(account_id=account.id, params={'dateTo': date_to}) + self.page.fill_coming(account) + self._update_iban(account, ibans) + yield account @need_login def iter_history(self, account): diff --git a/modules/ccf/pages.py b/modules/ccf/pages.py index 2c90ffbbf6abc68c6e27b301d72b82ec60d5e772..1e618c223795016f2a1cbef66d0a75e67b32e5de 100644 --- a/modules/ccf/pages.py +++ b/modules/ccf/pages.py @@ -28,18 +28,48 @@ Eval, Field, Format, + Lower, Map, + MapIn, Regexp, + Upper, ) from woob.browser.pages import JsonPage, LoggedPage +from woob.capabilities.bank import Account, AccountType, AccountOwnerType, AccountOwnership from woob.capabilities.bill import Document, DocumentTypes, Subscription from woob.tools.capabilities.bank.transactions import FrenchTransaction +from woob_modules.axabanque.pages.bank import AccountsPage as _AccountsPage, ACCOUNT_TYPES as ACCOUNT_TYPES_GENERIC class RibPage(LoggedPage, JsonPage): def get_iban(self): - return Dict("iban")(self.doc) + return Dict("iban", default=None)(self.doc) + + +ACCOUNT_TYPES_OVERRIDE = { + "checking": AccountType.CHECKING, + "stock": AccountType.PEA, # 'compte titres' and 'plan d'epargne en actions' have both the same type with the type field + "hav": AccountType.LIFE_INSURANCE, +} + +ACCOUNT_TYPES = {**ACCOUNT_TYPES_GENERIC, **ACCOUNT_TYPES_OVERRIDE} + +class AccountsPage(_AccountsPage): + @method + class iter_accounts(_AccountsPage.iter_accounts.klass): + class item(_AccountsPage.iter_accounts.klass.item): + obj_type = Coalesce( + MapIn(Lower(Dict('type')), ACCOUNT_TYPES, Account.TYPE_UNKNOWN), + MapIn(Lower(Dict('label')), ACCOUNT_TYPES, Account.TYPE_UNKNOWN), + default=Account.TYPE_UNKNOWN, + ) + obj__iban_encrypted = Dict("iban") + obj__iban_offended = Dict("offendedIBAN") + obj__contract_id = Dict("sourceContractId") + + def validate(self, obj): + return True class SubscriptionsPage(LoggedPage, JsonPage): @@ -48,16 +78,29 @@ class iter_subscriptions(DictElement): class item(ItemElement): klass = Subscription - obj_id = Dict("sourceContractId") + obj_id = Dict("accountId") + obj_label = Dict("label") # there can be several "participants" but no matter what _contract_id is, # list of related documents will be the same, so we can simply take the first one obj__contract_id = Dict("participants/0/id") # CAUTION non persistant - obj_subscriber = Format( - "%s %s", - CleanText(Dict("participants/0/firstName")), - CleanText(Dict("participants/0/lastName")), - ) + def obj_subscriber(self): + def key_participants(participant): + role = participant.get("role", None) + if role == "TIT": + role_idx = 0 + else: + role_idx = 1 + return '%s-%s-%s' % (role_idx, participant.get("lastName"), participant.get("firstName")) + + result = "" + for participant in sorted(Dict("participants")(self), key=key_participants): + result += Format( + "%s %s / ", + CleanText(Dict("lastName")), + CleanText(Dict("firstName")), + )(participant) + return result.strip("/ ") DOCUMENT_TYPES = { diff --git a/modules/ccf/requirements.txt b/modules/ccf/requirements.txt index 1ef9ec69aeef940f517291b33ae648a50354b369..2ab9712ca5ead082a41c1f2f25b4cf60ccec0a7b 100644 --- a/modules/ccf/requirements.txt +++ b/modules/ccf/requirements.txt @@ -1 +1,2 @@ woob ~= 3.6 +schwifty