<NME> leumi.ts
<BEF> import moment, { Moment } from 'moment';
import { Page } from 'puppeteer';
import { BaseScraperWithBrowser, LoginResults, LoginOptions } from './base-scraper-with-browser';
import {
  fillInput,
  clickButton,
  waitUntilElementFound,
  pageEvalAll,
} from '../helpers/elements-interactions';
import { SHEKEL_CURRENCY } from '../constants';
import {
} from '../helpers/elements-interactions';
import { waitForNavigation } from '../helpers/navigation';
import { SHEKEL_CURRENCY } from '../constants';
import { Transaction, TransactionStatuses, TransactionTypes } from '../types';

const BASE_URL = 'https://hb2.bankleumi.co.il';
const DATE_FORMAT = 'DD/MM/YY';

const DATE_FORMAT = 'DD.MM.YY';
const ACCOUNT_BLOCKED_MSG = 'המנוי חסום';
const INVALID_PASSWORD_MSG = 'אחד או יותר מפרטי ההזדהות שמסרת שגויים. ניתן לנסות שוב';


function getPossibleLoginResults() {
  const urls: LoginOptions['possibleResults'] = {
    [LoginResults.Success]: [/ebanking\/SO\/SPA.aspx/i],
    [LoginResults.InvalidPassword]: [
      async (options) => {
        if (!options || !options.page) {
          throw new Error('missing page options argument');
        }
        const errorMessage = await pageEvalAll(options.page, 'svg#Capa_1', '', (element) => {
          return (element[0]?.parentElement?.children[1] as HTMLDivElement)?.innerText;
        });

        return errorMessage?.startsWith(INVALID_PASSWORD_MSG);
      },
    ],
    [LoginResults.AccountBlocked]: [ // NOTICE - might not be relevant starting the Leumi re-design during 2022 Sep
      async (options) => {
        if (!options || !options.page) {
          throw new Error('missing page options argument');
        }
        const errorMessage = await pageEvalAll(options.page, '.errHeader', '', (label) => {
          return (label[0] as HTMLElement)?.innerText;
        });
  };
}

function convertTransactions(txns) {
  return txns.map((txn) => {
    const txnDate = moment(txn.date, DATE_FORMAT).toISOString();

    const credit = getAmountData(txn.credit).amount;
    const debit = getAmountData(txn.debit).amount;
    const amount = (Number.isNaN(credit) ? 0 : credit) - (Number.isNaN(debit) ? 0 : debit);
    return {
      type: TransactionTypes.Normal,
      identifier: txn.reference ? parseInt(txn.reference, 10) : null,
      date: txnDate,
      processedDate: txnDate,
      originalAmount: amount,
      originalCurrency: SHEKEL_CURRENCY,
      chargedAmount: amount,
      status: txn.status,
      description: txn.description,
      memo: txn.memo,
    };
  });
}

async function extractCompletedTransactionsFromPage(page) {
  const txns = [];
  const tdsValues = await pageEvalAll(page, '#WorkSpaceBox #ctlActivityTable tr td', [], (tds) => {
    return tds.map((td) => ({
      classList: td.getAttribute('class'),
  return new Promise<void>((resolve) => {
    setTimeout(() => {
      resolve();

  for (const element of tdsValues) {
    if (element.classList.includes('ExtendedActivityColumnDate')) {
      const newTransaction: Partial<Transaction> = { status: TransactionStatuses.Completed };
      newTransaction.date = (element.innerText || '').trim();
      txns.push(newTransaction);
    } else if (element.classList.includes('ActivityTableColumn1LTR') || element.classList.includes('ActivityTableColumn1')) {
      const changedTransaction = txns.pop();
      changedTransaction.description = element.innerText;
      txns.push(changedTransaction);
    } else if (element.classList.includes('ReferenceNumberUniqeClass')) {
      const changedTransaction = txns.pop();
      changedTransaction.reference = element.innerText;
      txns.push(changedTransaction);
    } else if (element.classList.includes('AmountDebitUniqeClass')) {
      const changedTransaction = txns.pop();
      changedTransaction.debit = element.innerText;
      txns.push(changedTransaction);
    } else if (element.classList.includes('AmountCreditUniqeClass')) {
      const changedTransaction = txns.pop();
      changedTransaction.credit = element.innerText;
      txns.push(changedTransaction);
    } else if (element.classList.includes('number_column')) {
      const changedTransaction = txns.pop();
      changedTransaction.balance = element.innerText;
      txns.push(changedTransaction);
    } else if (element.classList.includes('tdDepositRowAdded')) {
      const changedTransaction = txns.pop();
      changedTransaction.memo = (element.innerText || '').trim();
      txns.push(changedTransaction);
    }
  }

  return txns;
}

async function extractPendingTransactionsFromPage(page) {
  const txns = [];
  const tdsValues = await pageEvalAll(page, '#WorkSpaceBox #trTodayActivityNapaTableUpper tr td', [], (tds) => {
    return tds.map((td) => ({
      classList: td.getAttribute('class'),
  await clickButton(page, "button[aria-label='סנן']");
  const finalResponse = await page.waitForResponse((response) => {
    return response.url() === FILTERED_TRANSACTIONS_URL &&

  for (const element of tdsValues) {
    if (element.classList.includes('Colume1Width')) {
      const newTransaction: Partial<Transaction> = { status: TransactionStatuses.Pending };
      newTransaction.date = (element.innerText || '').trim();
      txns.push(newTransaction);
    } else if (element.classList.includes('Colume2Width')) {
      const changedTransaction = txns.pop();
      changedTransaction.description = element.innerText;
      txns.push(changedTransaction);
    } else if (element.classList.includes('Colume3Width')) {
      const changedTransaction = txns.pop();
      changedTransaction.reference = element.innerText;
      txns.push(changedTransaction);
    } else if (element.classList.includes('Colume4Width')) {
      const changedTransaction = txns.pop();
      changedTransaction.debit = element.innerText;
      txns.push(changedTransaction);
    } else if (element.classList.includes('Colume5Width')) {
      const changedTransaction = txns.pop();
      changedTransaction.credit = element.innerText;
      txns.push(changedTransaction);
    } else if (element.classList.includes('Colume6Width')) {
      const changedTransaction = txns.pop();
      changedTransaction.balance = element.innerText;
      txns.push(changedTransaction);
    }
  }


async function fetchTransactions(page: Page, startDate: Moment): Promise<TransactionsAccount[]> {
  const accounts: TransactionsAccount[] = [];

  // DEVELOPER NOTICE the account number received from the server is being altered at
  // runtime for some accounts after 1-2 seconds so we need to hang the process for a short while.
  await hangProcess(4000);

  const accountsIds = await page.evaluate(() => Array.from(document.querySelectorAll('app-masked-number-combo span.display-number-li'), (e) => e.textContent)) as string[];

  // due to a bug, the altered value might include undesired signs like & that should be removed
  return false;
}

async function fetchTransactionsForAccount(page, startDate, accountId) {
  await dropdownSelect(page, 'select#ddlAccounts_m_ddl', accountId);
  await dropdownSelect(page, 'select#ddlTransactionPeriod', '004');
  await waitUntilElementFound(page, 'select#ddlTransactionPeriod');
      // get list of accounts and check accountId
      await clickByXPath(page, '//*[contains(@class, "number") and contains(@class, "combo-inner")]');
      await clickByXPath(page, `//span[contains(text(), '${accountId}')]`);
    }

    accounts.push(await fetchTransactionsForAccount(page, startDate, removeSpecialCharacters(accountId)));
  }

  return accounts;
}


async function navigateToLogin(page: Page): Promise<void> {
  const loginButtonSelector = '#enter_your_account a';
  await waitUntilElementFound(page, loginButtonSelector);
  await clickButton(page, loginButtonSelector);
  await waitForNavigation(page, { waitUntil: 'networkidle2' });
  await Promise.all([
    waitUntilElementFound(page, 'input[placeholder="שם משתמש"]', true),
    waitUntilElementFound(page, 'input[placeholder="סיסמה"]', true),
    waitUntilElementFound(page, 'button[type="submit"]', true),
  ]);
}

async function waitForPostLogin(page: Page): Promise<void> {
  await Promise.race([
    waitUntilElementFound(page, 'a[title="דלג לחשבון"]', true, 60000),
    waitUntilElementFound(page, 'div.leumi-container', true, 60000),
    page.waitForXPath(`//div[contains(string(),"${INVALID_PASSWORD_MSG}")]`),
    waitUntilElementFound(page, 'form[action="/changepassword"]', true, 60000), // not sure if they kept this one
  ]);
}

class LeumiScraper extends BaseScraperWithBrowser {
  getLoginOptions(credentials: Record<string, string>) {
    return {
      loginUrl: LOGIN_URL,
      fields: createLoginFields(credentials),
      submitButtonSelector: "button[type='submit']",
      checkReadiness: async () => navigateToLogin(this.page),
      postAction: async () => waitForPostLogin(this.page),
      possibleResults: getPossibleLoginResults(),
    };
}

async function fetchTransactions(page, startDate) {
  const res = [];
  // Loop through all available accounts and collect transactions from all
  const accounts = await dropdownElements(page, 'select#ddlAccounts_m_ddl');
  for (const account of accounts) {

    await this.navigateTo(TRANSACTIONS_URL);

    const accounts = await fetchTransactions(this.page, startMoment);

    return {
      success: true,
      accounts,
    };
  }
}

export default LeumiScraper;
    };
  }

  async fetchData() {
    const defaultStartMoment = moment().subtract(1, 'years').add(1, 'day');
    const startDate = this.options.startDate || defaultStartMoment.toDate();
    const startMoment = moment.max(defaultStartMoment, moment(startDate));

<MSG> fix leumi scraper with strict typescript rule

<DFF> @@ -12,7 +12,9 @@ import {
 } from '../helpers/elements-interactions';
 import { waitForNavigation } from '../helpers/navigation';
 import { SHEKEL_CURRENCY } from '../constants';
-import { Transaction, TransactionStatuses, TransactionTypes } from '../types';
+import {
+  LegacyScrapingResult, ScraperAccount, Transaction, TransactionStatuses, TransactionTypes,
+} from '../types';
 
 const BASE_URL = 'https://hb2.bankleumi.co.il';
 const DATE_FORMAT = 'DD/MM/YY';
@@ -48,30 +50,43 @@ function getAmountData(amountStr) {
   };
 }
 
-function convertTransactions(txns) {
+interface SiteTransaction {
+  status: TransactionStatuses,
+  description?: string,
+  memo?: string,
+  balance?: string,
+  credit?: string,
+  debit?: string,
+  reference?: string,
+  date?: string,
+}
+
+function convertTransactions(txns: SiteTransaction[]): Transaction[] {
   return txns.map((txn) => {
     const txnDate = moment(txn.date, DATE_FORMAT).toISOString();
 
     const credit = getAmountData(txn.credit).amount;
     const debit = getAmountData(txn.debit).amount;
     const amount = (Number.isNaN(credit) ? 0 : credit) - (Number.isNaN(debit) ? 0 : debit);
-    return {
+
+    const transaction: Transaction = {
       type: TransactionTypes.Normal,
-      identifier: txn.reference ? parseInt(txn.reference, 10) : null,
+      identifier: txn.reference ? parseInt(txn.reference, 10) : undefined,
       date: txnDate,
       processedDate: txnDate,
       originalAmount: amount,
       originalCurrency: SHEKEL_CURRENCY,
       chargedAmount: amount,
       status: txn.status,
-      description: txn.description,
-      memo: txn.memo,
+      description: txn.description || '',
+      memo: txn.memo || '',
     };
+    return transaction;
   });
 }
 
-async function extractCompletedTransactionsFromPage(page) {
-  const txns = [];
+async function extractCompletedTransactionsFromPage(page): Promise<SiteTransaction[]> {
+  const txns: SiteTransaction[] = [];
   const tdsValues = await pageEvalAll(page, '#WorkSpaceBox #ctlActivityTable tr td', [], (tds) => {
     return tds.map((td) => ({
       classList: td.getAttribute('class'),
@@ -81,41 +96,35 @@ async function extractCompletedTransactionsFromPage(page) {
 
   for (const element of tdsValues) {
     if (element.classList.includes('ExtendedActivityColumnDate')) {
-      const newTransaction: Partial<Transaction> = { status: TransactionStatuses.Completed };
+      const newTransaction: SiteTransaction = { status: TransactionStatuses.Completed };
       newTransaction.date = (element.innerText || '').trim();
       txns.push(newTransaction);
-    } else if (element.classList.includes('ActivityTableColumn1LTR') || element.classList.includes('ActivityTableColumn1')) {
-      const changedTransaction = txns.pop();
-      changedTransaction.description = element.innerText;
-      txns.push(changedTransaction);
-    } else if (element.classList.includes('ReferenceNumberUniqeClass')) {
-      const changedTransaction = txns.pop();
-      changedTransaction.reference = element.innerText;
-      txns.push(changedTransaction);
-    } else if (element.classList.includes('AmountDebitUniqeClass')) {
-      const changedTransaction = txns.pop();
-      changedTransaction.debit = element.innerText;
-      txns.push(changedTransaction);
-    } else if (element.classList.includes('AmountCreditUniqeClass')) {
-      const changedTransaction = txns.pop();
-      changedTransaction.credit = element.innerText;
-      txns.push(changedTransaction);
-    } else if (element.classList.includes('number_column')) {
-      const changedTransaction = txns.pop();
-      changedTransaction.balance = element.innerText;
-      txns.push(changedTransaction);
-    } else if (element.classList.includes('tdDepositRowAdded')) {
-      const changedTransaction = txns.pop();
-      changedTransaction.memo = (element.innerText || '').trim();
-      txns.push(changedTransaction);
+    } else {
+      const changedTransaction = txns.length ? txns.pop() : null;
+      if (changedTransaction) {
+        if (element.classList.includes('ActivityTableColumn1LTR') || element.classList.includes('ActivityTableColumn1')) {
+          changedTransaction.description = element.innerText;
+        } else if (element.classList.includes('ReferenceNumberUniqeClass')) {
+          changedTransaction.reference = element.innerText;
+        } else if (element.classList.includes('AmountDebitUniqeClass')) {
+          changedTransaction.debit = element.innerText;
+        } else if (element.classList.includes('AmountCreditUniqeClass')) {
+          changedTransaction.credit = element.innerText;
+        } else if (element.classList.includes('number_column')) {
+          changedTransaction.balance = element.innerText;
+        } else if (element.classList.includes('tdDepositRowAdded')) {
+          changedTransaction.memo = (element.innerText || '').trim();
+        }
+        txns.push(changedTransaction);
+      }
     }
   }
 
   return txns;
 }
 
-async function extractPendingTransactionsFromPage(page) {
-  const txns = [];
+async function extractPendingTransactionsFromPage(page): Promise<SiteTransaction[]> {
+  const txns: SiteTransaction[] = [];
   const tdsValues = await pageEvalAll(page, '#WorkSpaceBox #trTodayActivityNapaTableUpper tr td', [], (tds) => {
     return tds.map((td) => ({
       classList: td.getAttribute('class'),
@@ -125,29 +134,26 @@ async function extractPendingTransactionsFromPage(page) {
 
   for (const element of tdsValues) {
     if (element.classList.includes('Colume1Width')) {
-      const newTransaction: Partial<Transaction> = { status: TransactionStatuses.Pending };
+      const newTransaction: SiteTransaction = { status: TransactionStatuses.Pending };
       newTransaction.date = (element.innerText || '').trim();
       txns.push(newTransaction);
-    } else if (element.classList.includes('Colume2Width')) {
-      const changedTransaction = txns.pop();
-      changedTransaction.description = element.innerText;
-      txns.push(changedTransaction);
-    } else if (element.classList.includes('Colume3Width')) {
-      const changedTransaction = txns.pop();
-      changedTransaction.reference = element.innerText;
-      txns.push(changedTransaction);
-    } else if (element.classList.includes('Colume4Width')) {
-      const changedTransaction = txns.pop();
-      changedTransaction.debit = element.innerText;
-      txns.push(changedTransaction);
-    } else if (element.classList.includes('Colume5Width')) {
-      const changedTransaction = txns.pop();
-      changedTransaction.credit = element.innerText;
-      txns.push(changedTransaction);
-    } else if (element.classList.includes('Colume6Width')) {
-      const changedTransaction = txns.pop();
-      changedTransaction.balance = element.innerText;
-      txns.push(changedTransaction);
+    } else {
+      const changedTransaction = txns.length ? txns.pop() : null;
+
+      if (changedTransaction) {
+        if (element.classList.includes('Colume2Width')) {
+          changedTransaction.description = element.innerText;
+        } else if (element.classList.includes('Colume3Width')) {
+          changedTransaction.reference = element.innerText;
+        } else if (element.classList.includes('Colume4Width')) {
+          changedTransaction.debit = element.innerText;
+        } else if (element.classList.includes('Colume5Width')) {
+          changedTransaction.credit = element.innerText;
+        } else if (element.classList.includes('Colume6Width')) {
+          changedTransaction.balance = element.innerText;
+        }
+        txns.push(changedTransaction);
+      }
     }
   }
 
@@ -165,7 +171,7 @@ async function isNoTransactionInDateRangeError(page) {
   return false;
 }
 
-async function fetchTransactionsForAccount(page, startDate, accountId) {
+async function fetchTransactionsForAccount(page, startDate, accountId): Promise<ScraperAccount> {
   await dropdownSelect(page, 'select#ddlAccounts_m_ddl', accountId);
   await dropdownSelect(page, 'select#ddlTransactionPeriod', '004');
   await waitUntilElementFound(page, 'select#ddlTransactionPeriod');
@@ -215,7 +221,7 @@ async function fetchTransactionsForAccount(page, startDate, accountId) {
 }
 
 async function fetchTransactions(page, startDate) {
-  const res = [];
+  const res: ScraperAccount[] = [];
   // Loop through all available accounts and collect transactions from all
   const accounts = await dropdownElements(page, 'select#ddlAccounts_m_ddl');
   for (const account of accounts) {
@@ -246,7 +252,7 @@ class LeumiScraper extends BaseScraperWithBrowser {
     };
   }
 
-  async fetchData() {
+  async fetchData(): Promise<LegacyScrapingResult> {
     const defaultStartMoment = moment().subtract(1, 'years').add(1, 'day');
     const startDate = this.options.startDate || defaultStartMoment.toDate();
     const startMoment = moment.max(defaultStartMoment, moment(startDate));

fix leumi scraper with strict typescript rule