Last active
July 14, 2019 07:35
-
-
Save esakal/dc23700a5f0689ae47291b98c9d424d2 to your computer and use it in GitHub Desktop.
eshaham/israeli-bank-scrapers#219 - leumi card scraper - broken scraper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// NOTICE - this scraper is using date range to fetch transactions. It breaks the installments support of our scrapers | |
// since it doesn't return installments beside the first one. | |
// see https://gist.github.com/esakal/75a291267530fa7a7980577ae5dd055c | |
import buildUrl from 'build-url'; | |
import moment from 'moment'; | |
import { fetchGetWithinPage } from '../helpers/fetch'; | |
import { BaseScraperWithBrowser, LOGIN_RESULT } from './base-scraper-with-browser'; | |
import { waitForRedirect } from '../helpers/navigation'; | |
import { waitUntilElementFound, elementPresentOnPage, clickButton } from '../helpers/elements-interactions'; | |
import { | |
NORMAL_TXN_TYPE, | |
INSTALLMENTS_TXN_TYPE, | |
TRANSACTION_STATUS, | |
} from '../constants'; | |
import { fixInstallments, sortTransactionsByDate, filterOldTransactions } from '../helpers/transactions'; | |
const REST_DATE_FORMAT = 'YYYY-MM-DD'; | |
const BASE_ACTIONS_URL = 'https://online.max.co.il'; | |
const BASE_API_ACTIONS_URL = 'https://onlinelcapi.max.co.il'; | |
const BASE_WELCOME_URL = 'https://www.max.co.il'; | |
const NORMAL_TYPE_NAME = 'רגילה'; | |
const ATM_TYPE_NAME = 'חיוב עסקות מיידי'; | |
const INTERNET_SHOPPING_TYPE_NAME = 'אינטרנט/חו"ל'; | |
const INSTALLMENTS_TYPE_NAME = 'תשלומים'; | |
const MONTHLY_CHARGE_TYPE_NAME = 'חיוב חודשי'; | |
const ONE_MONTH_POSTPONED_TYPE_NAME = 'דחוי חודש'; | |
const MONTHLY_POSTPONED_TYPE_NAME = 'דחוי לחיוב החודשי'; | |
const THIRTY_DAYS_PLUS_TYPE_NAME = 'עסקת 30 פלוס'; | |
const TWO_MONTHS_POSTPONED_TYPE_NAME = 'דחוי חודשיים'; | |
const MONTHLY_CHARGE_PLUS_INTEREST_TYPE_NAME = 'חודשי + ריבית'; | |
const CREDIT_TYPE_NAME = 'קרדיט'; | |
const INVALID_DETAILS_SELECTOR = '#popupWrongDetails'; | |
const LOGIN_ERROR_SELECTOR = '#popupCardHoldersLoginError'; | |
function redirectOrDialog(page) { | |
return Promise.race([ | |
waitForRedirect(page, 20000, false, [BASE_WELCOME_URL, `${BASE_WELCOME_URL}/`]), | |
waitUntilElementFound(page, INVALID_DETAILS_SELECTOR, true), | |
waitUntilElementFound(page, LOGIN_ERROR_SELECTOR, true), | |
]); | |
} | |
function getTransactionType(txnTypeStr) { | |
const cleanedUpTxnTypeStr = txnTypeStr.replace('\t', ' ').trim(); | |
switch (cleanedUpTxnTypeStr) { | |
case ATM_TYPE_NAME: | |
case NORMAL_TYPE_NAME: | |
case MONTHLY_CHARGE_TYPE_NAME: | |
case ONE_MONTH_POSTPONED_TYPE_NAME: | |
case MONTHLY_POSTPONED_TYPE_NAME: | |
case THIRTY_DAYS_PLUS_TYPE_NAME: | |
case TWO_MONTHS_POSTPONED_TYPE_NAME: | |
case INTERNET_SHOPPING_TYPE_NAME: | |
case MONTHLY_CHARGE_PLUS_INTEREST_TYPE_NAME: | |
return NORMAL_TXN_TYPE; | |
case INSTALLMENTS_TYPE_NAME: | |
case CREDIT_TYPE_NAME: | |
return INSTALLMENTS_TXN_TYPE; | |
default: | |
throw new Error(`Unknown transaction type ${cleanedUpTxnTypeStr}`); | |
} | |
} | |
function getInstallmentsInfo(comments) { | |
if (!comments) { | |
return null; | |
} | |
const matches = comments.match(/\d+/g); | |
if (!matches || matches.length < 2) { | |
return null; | |
} | |
return { | |
number: parseInt(matches[0], 10), | |
total: parseInt(matches[1], 10), | |
}; | |
} | |
function mapTransaction(rawTransaction) { | |
const isPending = rawTransaction.paymentDate === null; | |
const processedDate = moment(isPending ? | |
rawTransaction.purchaseDate : | |
rawTransaction.paymentDate).toISOString(); | |
const status = isPending ? TRANSACTION_STATUS.PENDING : TRANSACTION_STATUS.COMPLETED; | |
return { | |
type: getTransactionType(rawTransaction.planName), | |
date: moment(rawTransaction.purchaseDate).toISOString(), | |
processedDate, | |
originalAmount: -rawTransaction.originalAmount, | |
originalCurrency: rawTransaction.originalCurrency, | |
chargedAmount: -rawTransaction.actualPaymentAmount, | |
description: rawTransaction.merchantName.trim(), | |
memo: rawTransaction.comments, | |
installments: getInstallmentsInfo(rawTransaction.comments), | |
status, | |
}; | |
} | |
function prepareTransactions(txns, startMoment, combineInstallments) { | |
let clonedTxns = Array.from(txns); | |
if (!combineInstallments) { | |
clonedTxns = fixInstallments(clonedTxns); | |
} | |
clonedTxns = sortTransactionsByDate(clonedTxns); | |
clonedTxns = filterOldTransactions(clonedTxns, startMoment, combineInstallments); | |
return clonedTxns; | |
} | |
async function fetchTransactions(page, startMoment) { | |
const date = moment().subtract(1, 'month').endOf('month').format(REST_DATE_FORMAT); | |
const startDate = startMoment.format(REST_DATE_FORMAT); | |
const endDate = moment().format(REST_DATE_FORMAT); | |
/** | |
* url explanation: | |
* userIndex: -1 for all account owners | |
* cardIndex: -1 for all cards under the account | |
* monthView: false, we will use date range instead | |
* date: last day of previous month, cannot be null but doesn't affect the range. | |
* dates: the actual date range to be used | |
*/ | |
const url = buildUrl(BASE_API_ACTIONS_URL, { | |
path: `/api/registered/transactionDetails/getTransactionsAndGraphs?filterData={"userIndex":-1,"cardIndex":-1,"monthView":false,"date":"${date}","dates":{"startDate":"${startDate}","endDate":"${endDate}"}}&v=V3.13-HF.6.26`, | |
}); | |
const data = await fetchGetWithinPage(page, url); | |
const result = {}; | |
data.result.transactions.forEach((transaction) => { | |
if (!result[transaction.shortCardNumber]) { | |
result[transaction.shortCardNumber] = []; | |
} | |
const mappedTransaction = mapTransaction(transaction); | |
result[transaction.shortCardNumber].push(mappedTransaction); | |
}); | |
return result; | |
} | |
function getPossibleLoginResults(page) { | |
const urls = {}; | |
urls[LOGIN_RESULT.SUCCESS] = [`${BASE_WELCOME_URL}/homepage/personal`]; | |
urls[LOGIN_RESULT.CHANGE_PASSWORD] = [`${BASE_ACTIONS_URL}/Anonymous/Login/PasswordExpired.aspx`]; | |
urls[LOGIN_RESULT.INVALID_PASSWORD] = [async () => { | |
return elementPresentOnPage(page, INVALID_DETAILS_SELECTOR); | |
}]; | |
urls[LOGIN_RESULT.UNKNOWN_ERROR] = [async () => { | |
return elementPresentOnPage(page, LOGIN_ERROR_SELECTOR); | |
}]; | |
return urls; | |
} | |
function createLoginFields(inputGroupName, credentials) { | |
return [ | |
{ selector: `#${inputGroupName}_txtUserName`, value: credentials.username }, | |
{ selector: '#txtPassword', value: credentials.password }, | |
]; | |
} | |
class LeumiCardScraper extends BaseScraperWithBrowser { | |
getLoginOptions(credentials) { | |
const inputGroupName = 'PlaceHolderMain_CardHoldersLogin1'; | |
return { | |
loginUrl: `${BASE_ACTIONS_URL}/Anonymous/Login/CardholdersLogin.aspx`, | |
fields: createLoginFields(inputGroupName, credentials), | |
submitButtonSelector: `#${inputGroupName}_btnLogin`, | |
preAction: async () => { | |
if (await elementPresentOnPage(this.page, '#closePopup')) { | |
await clickButton(this.page, '#closePopup'); | |
} | |
}, | |
postAction: async () => redirectOrDialog(this.page), | |
possibleResults: getPossibleLoginResults(this.page), | |
}; | |
} | |
async fetchData() { | |
const defaultStartMoment = moment().subtract(1, 'years'); | |
const startDate = this.options.startDate || defaultStartMoment.toDate(); | |
const startMoment = moment.max(defaultStartMoment, moment(startDate)); | |
const results = await fetchTransactions(this.page, startMoment); | |
const accounts = Object.keys(results).map((accountNumber) => { | |
const txns = prepareTransactions(results[accountNumber], | |
startMoment, this.options.combineInstallments); | |
return { | |
accountNumber, | |
txns, | |
}; | |
}); | |
return { | |
success: true, | |
accounts, | |
}; | |
} | |
} | |
export default LeumiCardScraper; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment