Skip to content

Instantly share code, notes, and snippets.

@esakal
Last active July 14, 2019 07:35
Show Gist options
  • Save esakal/dc23700a5f0689ae47291b98c9d424d2 to your computer and use it in GitHub Desktop.
Save esakal/dc23700a5f0689ae47291b98c9d424d2 to your computer and use it in GitHub Desktop.
eshaham/israeli-bank-scrapers#219 - leumi card scraper - broken scraper
// 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