<NME> leumi.ts
<BEF> import 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 {
  TransactionsAccount, Transaction, TransactionStatuses, TransactionTypes,
} from '../transactions';
import { ScaperScrapingResult, ScraperCredentials } from './base-scraper';
import {
  LegacyScrapingResult, ScraperAccount, 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) {
  return urls;
}

function createLoginFields(credentials) {
  return [
    { selector: '#wtr_uid', value: credentials.username },
    { selector: '#wtr_password', value: credentials.password },
  ];
}

function getAmountData(amountStr) {
  const amountStrCopy = amountStr.replace(',', '');
  const amount = parseFloat(amountStrCopy);
  const currency = SHEKEL_CURRENCY;
        const errorMessage = await pageEvalAll(options.page, '.errHeader', '', (label) => {
          return (label[0] as HTMLElement)?.innerText;
        });

        return errorMessage?.startsWith(ACCOUNT_BLOCKED_MSG);
      },
    ],
    [LoginResults.ChangePassword]: ['https://hb2.bankleumi.co.il/authenticate'], // NOTICE - might not be relevant starting the Leumi re-design during 2022 Sep
  };
  return urls;
}

function createLoginFields(credentials: ScraperCredentials) {
  return [
    { selector: 'input[placeholder="שם משתמש"]', value: credentials.username },
    { selector: 'input[placeholder="סיסמה"]', value: credentials.password },
  ];
}

  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);

    const transaction: Transaction = {
      status,
      type: TransactionTypes.Normal,
      date,
      processedDate: date,
      description: rawTransaction.Description || '',
      identifier: rawTransaction.ReferenceNumberLong,
      memo: rawTransaction.AdditionalData || '',
      originalCurrency: SHEKEL_CURRENCY,
      chargedAmount: rawTransaction.Amount,
      originalAmount: rawTransaction.Amount,
    };
    return transaction;
  });
}

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

  await page.waitForXPath(xpath, { timeout: 30000, visible: true });
  const elm = await page.$x(xpath);
  await elm[0].click();
}

function removeSpecialCharacters(str: string): string {
  return str.replace(/[^0-9/-]/g, '');
}

async function fetchTransactionsForAccount(page: Page, startDate: Moment, accountId: string): Promise<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);

  await waitUntilElementFound(page, 'button[title="חיפוש מתקדם"]', true);
  await clickButton(page, 'button[title="חיפוש מתקדם"]');
  await waitUntilElementFound(page, 'bll-radio-button', true);
  await clickButton(page, 'bll-radio-button:not([checked])');

  await waitUntilElementFound(page, 'input[formcontrolname="txtInputFrom"]', true);

  await fillInput(
    page,
    'input[formcontrolname="txtInputFrom"]',
    startDate.format(DATE_FORMAT),
  );
  return txns;
}

async function extractPendingTransactionsFromPage(page): Promise<ScrapedTransaction[]> {
  const txns: ScrapedTransaction[] = [];
  const tdsValues = await pageEvalAll(page, '#WorkSpaceBox #trTodayActivityNapaTableUpper tr td', [], (tds) => {
    return tds.map((td) => ({
      classList: td.getAttribute('class'),
      innerText: td.innerText,
    }));
  });

  const accountNumber = accountId.replace('/', '_').replace(/[^\d-_]/g, '');

  const response = JSON.parse(responseJson.jsonResp);

  const pendingTransactions = response.TodayTransactionsItems;
  const transactions = response.HistoryTransactionsItems;
  const balance = response.BalanceDisplay ? parseFloat(response.BalanceDisplay) : undefined;

  const pendingTxns = extractTransactionsFromPage(pendingTransactions, TransactionStatuses.Pending);
  const completedTxns = extractTransactionsFromPage(transactions, TransactionStatuses.Completed);
  const txns = [
    ...pendingTxns,
    ...completedTxns,
  ];

  return {
    accountNumber,
    balance,
    txns,
  };
}

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

  return txns;
}

async function isNoTransactionInDateRangeError(page) {
  const hasErrorInfoElement = await elementPresentOnPage(page, '.errInfo');
  if (hasErrorInfoElement) {
    const errorText = await page.$eval('.errInfo', (errorElement) => {
      return errorElement.innerText;
    });
    return errorText === NO_TRANSACTION_IN_DATE_RANGE_TEXT;
  }
  return false;
}

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');

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

  return accounts;
}
  await waitForNavigation(page);

  const selectedSnifAccount = await page.$eval('#ddlAccounts_m_ddl option[selected="selected"]', (option) => {
    return option.innerText;
  });

  const accountNumber = selectedSnifAccount.replace('/', '_');
  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 fetchData(): Promise<ScaperScrapingResult> {
  };
}

async function fetchTransactions(page, startDate) {
  const res: ScraperAccount[] = [];
  // Loop through all available accounts and collect transactions from all
  const accounts = await dropdownElements(page, 'select#ddlAccounts_m_ddl');
    const accounts = await fetchTransactions(this.page, startMoment);

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

export default LeumiScraper;
}

class LeumiScraper extends BaseScraperWithBrowser {
  getLoginOptions(credentials) {
    return {
      loginUrl: `${BASE_URL}`,
      fields: createLoginFields(credentials),

<MSG> fix leumi types

<DFF> @@ -1,4 +1,4 @@
-import moment from 'moment';
+import moment, { Moment } from 'moment';
 import { Page } from 'puppeteer';
 import { BaseScraperWithBrowser, LoginResults, LoginOptions } from './base-scraper-with-browser';
 import {
@@ -15,6 +15,7 @@ import { SHEKEL_CURRENCY } from '../constants';
 import {
   LegacyScrapingResult, ScraperAccount, Transaction, TransactionStatuses, TransactionTypes,
 } from '../types';
+import { ScraperCredentials } from './base-scraper';
 
 const BASE_URL = 'https://hb2.bankleumi.co.il';
 const DATE_FORMAT = 'DD/MM/YY';
@@ -32,14 +33,14 @@ function getPossibleLoginResults() {
   return urls;
 }
 
-function createLoginFields(credentials) {
+function createLoginFields(credentials: ScraperCredentials) {
   return [
     { selector: '#wtr_uid', value: credentials.username },
     { selector: '#wtr_password', value: credentials.password },
   ];
 }
 
-function getAmountData(amountStr) {
+function getAmountData(amountStr: string) {
   const amountStrCopy = amountStr.replace(',', '');
   const amount = parseFloat(amountStrCopy);
   const currency = SHEKEL_CURRENCY;
@@ -65,8 +66,8 @@ function convertTransactions(txns: ScrapedTransaction[]): 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 credit = getAmountData(txn.credit || '').amount;
+    const debit = getAmountData(txn.debit || '').amount;
     const amount = (Number.isNaN(credit) ? 0 : credit) - (Number.isNaN(debit) ? 0 : debit);
 
     const transaction: Transaction = {
@@ -84,13 +85,16 @@ function convertTransactions(txns: ScrapedTransaction[]): Transaction[] {
     return transaction;
   });
 }
-
-async function extractCompletedTransactionsFromPage(page): Promise<ScrapedTransaction[]> {
+interface ScrapedTds {
+  classList: string,
+  innerText: string
+}
+async function extractCompletedTransactionsFromPage(page: Page): Promise<ScrapedTransaction[]> {
   const txns: ScrapedTransaction[] = [];
-  const tdsValues = await pageEvalAll(page, '#WorkSpaceBox #ctlActivityTable tr td', [], (tds) => {
+  const tdsValues = await pageEvalAll<ScrapedTds[]>(page, '#WorkSpaceBox #ctlActivityTable tr td', [], (tds) => {
     return tds.map((td) => ({
-      classList: td.getAttribute('class'),
-      innerText: td.innerText,
+      classList: td.getAttribute('class') || '',
+      innerText: (td as HTMLElement).innerText,
     }));
   });
 
@@ -123,12 +127,12 @@ async function extractCompletedTransactionsFromPage(page): Promise<ScrapedTransa
   return txns;
 }
 
-async function extractPendingTransactionsFromPage(page): Promise<ScrapedTransaction[]> {
+async function extractPendingTransactionsFromPage(page: Page): Promise<ScrapedTransaction[]> {
   const txns: ScrapedTransaction[] = [];
-  const tdsValues = await pageEvalAll(page, '#WorkSpaceBox #trTodayActivityNapaTableUpper tr td', [], (tds) => {
+  const tdsValues = await pageEvalAll<ScrapedTds[]>(page, '#WorkSpaceBox #trTodayActivityNapaTableUpper tr td', [], (tds) => {
     return tds.map((td) => ({
-      classList: td.getAttribute('class'),
-      innerText: td.innerText,
+      classList: td.getAttribute('class') || '',
+      innerText: (td as HTMLElement).innerText,
     }));
   });
 
@@ -160,18 +164,18 @@ async function extractPendingTransactionsFromPage(page): Promise<ScrapedTransact
   return txns;
 }
 
-async function isNoTransactionInDateRangeError(page) {
+async function isNoTransactionInDateRangeError(page: Page) {
   const hasErrorInfoElement = await elementPresentOnPage(page, '.errInfo');
   if (hasErrorInfoElement) {
     const errorText = await page.$eval('.errInfo', (errorElement) => {
-      return errorElement.innerText;
+      return (errorElement as HTMLElement).innerText;
     });
     return errorText === NO_TRANSACTION_IN_DATE_RANGE_TEXT;
   }
   return false;
 }
 
-async function fetchTransactionsForAccount(page, startDate, accountId): Promise<ScraperAccount> {
+async function fetchTransactionsForAccount(page: Page, startDate: Moment, accountId: string): Promise<ScraperAccount> {
   await dropdownSelect(page, 'select#ddlAccounts_m_ddl', accountId);
   await dropdownSelect(page, 'select#ddlTransactionPeriod', '004');
   await waitUntilElementFound(page, 'select#ddlTransactionPeriod');
@@ -184,7 +188,7 @@ async function fetchTransactionsForAccount(page, startDate, accountId): Promise<
   await waitForNavigation(page);
 
   const selectedSnifAccount = await page.$eval('#ddlAccounts_m_ddl option[selected="selected"]', (option) => {
-    return option.innerText;
+    return (option as HTMLElement).innerText;
   });
 
   const accountNumber = selectedSnifAccount.replace('/', '_');
@@ -220,7 +224,7 @@ async function fetchTransactionsForAccount(page, startDate, accountId): Promise<
   };
 }
 
-async function fetchTransactions(page, startDate) {
+async function fetchTransactions(page: Page, startDate: Moment) {
   const res: ScraperAccount[] = [];
   // Loop through all available accounts and collect transactions from all
   const accounts = await dropdownElements(page, 'select#ddlAccounts_m_ddl');
@@ -242,7 +246,7 @@ async function waitForPostLogin(page: Page): Promise<void> {
 }
 
 class LeumiScraper extends BaseScraperWithBrowser {
-  getLoginOptions(credentials) {
+  getLoginOptions(credentials: Record<string, string>) {
     return {
       loginUrl: `${BASE_URL}`,
       fields: createLoginFields(credentials),

fix leumi types