Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fetch credit card line of credit utilization #857

Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion src/scrapers/base-isracard-amex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ interface ScrapedAccount {
processedDate: string;
}

interface AccountCredit {
creditUtilization: number;
creditLimit: number;
}
Comment on lines +64 to +67
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can declare this interface once in transactions.ts and import it everywhere.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though about that too, but as this is an internal type (it's used locally, not "passed" outside the file) I opted into this.
If you think otherwise - let me know.


interface ScrapedLoginValidation {
Header: {
Status: string;
Expand All @@ -84,6 +89,19 @@ interface ScrapedAccountsWithinPageResponse {
};
}

interface ScrapedCreditDataWithinPageResponse {
Header: {
Status: string;
};
RikuzNetuneyCreditDigiBean: {
cardsTotal: {
cardNumberTail: string;
nitzulLoCredit: string;
misgeretKolelet: string;
}[];
};
}

interface ScrapedCurrentCardTransactions {
txnIsrael?: ScrapedTransaction[];
txnAbroad?: ScrapedTransaction[];
Expand All @@ -98,6 +116,39 @@ interface ScrapedTransactionData {
}>;
}

function getCreditUtilizationUrl(servicesUrl: string) {
return buildUrl(servicesUrl, {
queryParams: {
reqName: 'RikuzNetuneyCreditDigi',
},
});
}

async function fetchCreditUtilization(
page: Page,
servicesUrl: string,
): Promise<_.Dictionary<AccountCredit> | null> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use standard types, I'm not familiar with lodash types, I can't understand them from the tooltip and I don't expect other contributors to understand it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As lodash is already installed on the project I believed it's OK.
Will change

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, although I'm thinking a lot before I add lodash to a project, if it is already there I accept using it.

But still, for the function signature, which is a kind of contract/interface, I prefer to use standard types.

const dataUrl = getCreditUtilizationUrl(servicesUrl);
const dataResult = await fetchGetWithinPage<ScrapedCreditDataWithinPageResponse>(
page,
dataUrl,
);

if (!dataResult) {
throw new Error('Failed to fetch credit utilization data, empty response');
}
baruchiro marked this conversation as resolved.
Show resolved Hide resolved

return _.fromPairs(
dataResult.RikuzNetuneyCreditDigiBean.cardsTotal.map((item) => [
item.cardNumberTail,
{
creditUtilization: parseFloat(item.nitzulLoCredit),
creditLimit: parseFloat(item.misgeretKolelet.replace(/,/g, '')),
},
]),
);
}

function getAccountsUrl(servicesUrl: string, monthMoment: Moment) {
const billingDate = monthMoment.format('YYYY-MM-DD');
return buildUrl(servicesUrl, {
Expand Down Expand Up @@ -284,6 +335,12 @@ function getExtraScrap(accountsWithIndex: ScrapedAccountsWithIndex[], page: Page
async function fetchAllTransactions(page: Page, options: ExtendedScraperOptions, startMoment: Moment) {
const futureMonthsToScrape = options.futureMonthsToScrape ?? 1;
const allMonths = getAllMonthMoments(startMoment, futureMonthsToScrape);
let creditUtilization: _.Dictionary<AccountCredit> | null = null;
if (options.includeCreditUtilization) {
debug('Getting credit utilization data');
creditUtilization = await fetchCreditUtilization(page, options.servicesUrl);
}

const results: ScrapedAccountsWithIndex[] = await Promise.all(allMonths.map(async (monthMoment) => {
return fetchTransactions(page, options, startMoment, monthMoment);
}));
Expand All @@ -306,10 +363,17 @@ async function fetchAllTransactions(page: Page, options: ExtendedScraperOptions,
});

const accounts = Object.keys(combinedTxns).map((accountNumber) => {
return {
const account: TransactionsAccount = {
accountNumber,
txns: combinedTxns[accountNumber],
};
if (creditUtilization && creditUtilization[accountNumber]) {
account.credit = {
creditUtilization: creditUtilization[accountNumber].creditUtilization,
creditLimit: creditUtilization[accountNumber].creditLimit,
};
}
Comment on lines +370 to +375
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about this?

Suggested change
if (creditUtilization && creditUtilization[accountNumber]) {
account.credit = {
creditUtilization: creditUtilization[accountNumber].creditUtilization,
creditLimit: creditUtilization[accountNumber].creditLimit,
};
}
account.credit = creditUtilization?.[accountNumber];
  1. The creditUtilization[accountNumber] structure is the same as the account.credit. Even if it is null or undefined it is OK because we accept no response for some accounts.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a better idea, Will update.

return account;
});

return {
Expand Down
5 changes: 5 additions & 0 deletions src/scrapers/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ export interface ScraperOptions {
*/
combineInstallments?: boolean;

/**
* if set to true, will fetch the credit utilization of the account (credit card only)
*/
includeCreditUtilization?: boolean;

/**
* additional arguments to pass to the browser instance. The list of flags can be found in
*
Expand Down
65 changes: 63 additions & 2 deletions src/scrapers/max.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import _ from 'lodash';
import buildUrl from 'build-url';
import moment, { Moment } from 'moment';
import { Page, LoadEvent } from 'puppeteer';
Expand All @@ -7,7 +8,10 @@ import { waitForRedirect } from '../helpers/navigation';
import { waitUntilElementFound, elementPresentOnPage, clickButton } from '../helpers/elements-interactions';
import getAllMonthMoments from '../helpers/dates';
import { fixInstallments, sortTransactionsByDate, filterOldTransactions } from '../helpers/transactions';
import { Transaction, TransactionStatuses, TransactionTypes } from '../transactions';
import {
Transaction, TransactionStatuses,
TransactionTypes, TransactionsAccount,
} from '../transactions';
import { getDebug } from '../helpers/debug';
import { ScraperOptions } from './interface';
import { SHEKEL_CURRENCY, DOLLAR_CURRENCY, EURO_CURRENCY } from '../constants';
Expand Down Expand Up @@ -95,6 +99,51 @@ function getTransactionsUrl(monthMoment: Moment) {
});
}

function getCreditUtilizationUrl() {
return buildUrl(BASE_API_ACTIONS_URL, {
path: '/api/registered/getHomePageData',
});
}

interface ScrapedCreditDataWithinPageResponse {
Result: {
UserCards: {
Cards: {
Last4Digits: string;
CreditLimit: number;
OpenToBuy: number;
}[];
};
};
}

interface AccountCredit {
creditUtilization: number;
creditLimit: number;
}

async function fetchCreditUtilization(
page: Page,
): Promise<_.Dictionary<AccountCredit>> {
const dataUrl = getCreditUtilizationUrl();
const dataResult = await fetchGetWithinPage<ScrapedCreditDataWithinPageResponse>(
page,
dataUrl,
);
if (!dataResult) {
throw new Error('Failed to fetch credit utilization data, empty response');
}
return _.fromPairs(
dataResult.Result.UserCards.Cards.map((item) => [
item.Last4Digits,
{
creditUtilization: item.CreditLimit - item.OpenToBuy,
creditLimit: item.CreditLimit,
},
]),
);
}

interface FetchCategoryResult {
result? : Array<{
id: number;
Expand Down Expand Up @@ -343,11 +392,23 @@ class MaxScraper extends BaseScraperWithBrowser<ScraperSpecificCredentials> {

async fetchData() {
const results = await fetchTransactions(this.page, this.options);
let creditUtilization: _.Dictionary<AccountCredit> | null = null;
if (this.options.includeCreditUtilization) {
debug('Getting credit utilization data');
creditUtilization = await fetchCreditUtilization(this.page);
}
const accounts = Object.keys(results).map((accountNumber) => {
return {
const account: TransactionsAccount = {
accountNumber,
txns: results[accountNumber],
};
if (creditUtilization && creditUtilization[accountNumber]) {
account.credit = {
creditUtilization: creditUtilization[accountNumber].creditUtilization,
creditLimit: creditUtilization[accountNumber].creditLimit,
};
}
return account;
});

return {
Expand Down
68 changes: 66 additions & 2 deletions src/scrapers/visa-cal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ScraperScrapingResult } from './interface';

const LOGIN_URL = 'https://www.cal-online.co.il/';
const TRANSACTIONS_REQUEST_ENDPOINT = 'https://api.cal-online.co.il/Transactions/api/transactionsDetails/getCardTransactionsDetails';
const CREDIT_FRAME_ENDPOINT = 'https://api.cal-online.co.il/Frames/api/Frames/GetFrameStatus';

const InvalidPasswordMessage = 'שם המשתמש או הסיסמה שהוזנו שגויים';

Expand Down Expand Up @@ -85,11 +86,39 @@ interface InitResponse {
}[];
};
}

interface AccountCredit {
creditUtilization: number;
creditLimit: number;
}


type CurrencySymbol = '₪' | string;
interface CardTransactionDetailsError {
title: string;
statusCode: number;
}

interface CardFrameStatusError {
title: string;
statusCode: number;
statusDescription: string;
}

interface CardFrameStatus extends CardFrameStatusError {
result: {
calIssuedCards: {
fictiveMaxAccAmt: number;
frameLimitForCardAmount: number;
totalUsageAmountForAccountManagementLevel: number;
nextTotalDebitForAccount: number;
};
};
statusCode: 1;
statusDescription: string;
statusTitle: string;
}

interface CardTransactionDetails extends CardTransactionDetailsError {
result: {
bankAccounts: {
Expand Down Expand Up @@ -238,6 +267,34 @@ function convertParsedDataToTransactions(parsedData: CardTransactionDetails[]):
});
}

async function fetchCreditUtilization(
page: Page,
Authorization: string,
xSiteId: string,
card: { cardUniqueId: string, last4Digits: string },
): Promise<AccountCredit> {
const dataResult = await fetchPostWithinPage<CardFrameStatus | CardFrameStatusError>(
page,
CREDIT_FRAME_ENDPOINT,
{ cardsForFrameData: [{ cardUniqueId: card.cardUniqueId }] },
{
Authorization,
'X-Site-Id': xSiteId,
'Content-Type': 'application/json',
},
);

if (dataResult?.statusCode !== 1) {
throw new Error(`failed to fetch frame data. Message: ${dataResult?.title || ''}`);
}

const utilizationData = (dataResult as CardFrameStatus).result.calIssuedCards;
return {
creditUtilization: utilizationData.nextTotalDebitForAccount,
creditLimit: utilizationData.frameLimitForCardAmount,
};
}

type ScraperSpecificCredentials = { username: string, password: string };

class VisaCalScraper extends BaseScraperWithBrowser<ScraperSpecificCredentials> {
Expand Down Expand Up @@ -376,10 +433,17 @@ class VisaCalScraper extends BaseScraperWithBrowser<ScraperSpecificCredentials>
filterOldTransactions(transactions, moment(startDate), this.options.combineInstallments || false) :
transactions;

return {
const account: TransactionsAccount = {
txns,
accountNumber: card.last4Digits,
} as TransactionsAccount;
};

if (this.options.includeCreditUtilization) {
debug('Getting credit utilization data');
account.credit = await fetchCreditUtilization(this.page, Authorization, xSiteId, card);
}

return account;
}),
);

Expand Down
9 changes: 9 additions & 0 deletions src/transactions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@

export interface TransactionsAccount {
accountNumber: string;

/**
* Relevant only for credit cards, this is the line of credit utilization, and credit limit.
* Only fetched if `includeCreditUtilization` is set to true in the scraper options.
*/
credit?: {
creditUtilization: number;
creditLimit: number;
};
balance?: number;
txns: Transaction[];
}
Expand Down
Loading