banker
Version:
Downloads transactions and balances from online banking websites using Zombie.js.
347 lines (284 loc) • 11.8 kB
JavaScript
var async = require('async'),
BankSession = require('./base'),
moment = require('moment'),
util = require('util'),
utils = require('../utils');
function TVFCUSession(config) {
var self = this;
BankSession.call(self, config);
self.loginUrl = 'https://ol24.tvfcu.com/Login.aspx';
self.accountsUrl = 'https://ol24.tvfcu.com/Accounts/Summary.aspx';
self.exportUrl = 'https://ol24.tvfcu.com/Accounts/TransactionDownload.aspx';
self.logoutUrl = 'https://ol24.tvfcu.com/Logout.aspx';
// Selector for the security question, if any
self.securityQSel = 'div[data-name=authorization-answer-input] div.editor-field';
// Selector for the security question answer
self.securityASel = '#authorizationControl_authorizationAnswer_txtInput';
// Selector for the security question submit button
// Note - this is not the selector for the button that actually appears on
// the page if you use a browser. That selector would be:
// input[type=submit][name="authorizationControl:_ctl9"]
self.securityBtnSel = '#btnAuthorizeNext';
// These pages take a while to fully load due to all the ASP.NET JavaScript
// stuff going on after the HTML loads.
self.waitOptions = {
duration : 10000
};
self.waitOptions2 = {
duration : 20000
};
}
util.inherits(TVFCUSession, BankSession);
TVFCUSession.prototype.info = function() {
return {
description : 'Tennessee Valley Federal Credit Union (http://tvfcu.com/)',
configItems : {
bankName : '"tvfcu"',
username : 'Online banking username',
password : 'Online banking password',
securityQuestions : 'Hash of security questions and answers'
},
accountItems : {
description : 'Link text on Accounts page (e.g. "SHARE ACCOUNT (*NNNNN)")'
}
};
};
TVFCUSession.prototype.login = function(cb) {
var self = this;
if (self.loggedIn) {
self.log('already logged in');
cb(null);
return;
}
self.log('visiting login page');
self.browser.visit(self.loginUrl, function() {
if (!self.pageLoaded('login', cb)) return;
self.log('sending username and password');
try {
self.browser.fill('#txtUsername_txtInput', self.config.username);
self.browser.fill('#txtPassword_txtInput', self.config.password);
self.browser.click('#btnLogin');
} catch (err) {
return cb(err);
}
self.browser.wait(self.waitOptions, function() {
if (!self.pageLoaded('afterLogin0', cb)) return;
var qa = self.browser.querySelectorAll(self.securityQSel);
if (qa && qa.length) {
if (!self.pageLoaded('securityQuestion', cb)) return;
var question = self.browser.text(qa[0]);
self.log('got security question: ' + question);
if (self.config.securityQuestions) {
var answer = self.config.securityQuestions[question];
if (answer) {
self.log('answering security question');
self.browser.once('request', function(request) {
// The server really doesn't like that Zombie.js
// inclues this parameter (with an empty value).
delete request.params['authorizationControl:authorizationPhone'];
});
self.browser.fill(self.securityASel, answer);
self.browser.click(self.securityBtnSel);
self.browser.wait(self.waitOptions, function() {
self._loggedIn(cb);
});
} else {
cb(new Error('Missing answer to security question: ' + question));
return;
}
} else {
cb(new Error('Missing config.securityQuestions'));
return;
}
} else {
self._loggedIn(cb);
}
});
});
};
TVFCUSession.prototype._loggedIn = function(cb) {
var self = this;
if (!self.pageLoaded('afterLogin', cb)) return;
var link = self.browser.link('Logout');
if (!link || !link.href) {
cb(new Error(self.browser.alertMessage || 'Cannot find Logout link.'));
return;
}
self.loggedIn = true;
self.log('logged in');
cb(null);
};
TVFCUSession.prototype.getTransactions = function(account, cb) {
var self = this;
self.log('visiting accounts page');
self.browser.visit(self.accountsUrl, self.waitOptions, function() {
if (!self.pageLoaded('accounts', cb)) return;
var link = null,
links = self.browser.querySelectorAll(
'table.dataTable a[href^="/Accounts/Details.ashx"]');
for (var i = 0; i < links.length; i++) {
if (self.browser.text(links[i]) == account.description) {
link = links[i];
break;
}
}
if (!link) {
cb(new Error('Cannot find account with description "' + account.description + '".'));
return;
}
self.log('clicking account link (description "' + account.description + '")');
self.browser.visit(link.href, self.waitOptions, function() {
if (!self.pageLoaded('account', cb)) return;
if (account.minDate || account.maxDate) {
var minDateSel = '#dtFilterFromDate_dtInput_TextBox',
maxDateSel = '#dtFilterToDate_dtInput_TextBox';
if (account.minDate) {
self.browser.fill(minDateSel, account.minDate);
}
if (account.maxDate) {
self.browser.fill(maxDateSel, account.maxDate);
}
var minDate = self.browser.query(minDateSel).value,
maxDate = self.browser.query(maxDateSel).value;
self.log('getting data from ' + minDate + ' to ' + maxDate);
self.browser.click('#btnFilterSubmit');
self.browser.wait(self.waitOptions2, function() {
if (!self.pageLoaded('accountExpanded', cb)) return;
self._scrapeTransactions(cb);
});
} else {
self._scrapeTransactions(cb);
}
});
});
};
TVFCUSession.prototype._scrapeTransactions = function(cb) {
var self = this;
var data = {
transactions : [],
balances : {}
},
checkLinks = [];
var cells = self.browser.querySelectorAll('div.summaryBox table.summaryTable td');
for (var i = 0; i < cells.length; i++) {
switch (self.browser.text(cells[i])) {
case 'Available Balance':
data.balances.available = self.getBalance(cells[i + 1]);
break;
case 'Current Balance':
data.balances.actual = self.getBalance(cells[i + 1]);
break;
}
}
// Scrape transaction data directly from the account page instead of using
// the export function, because the account page has both Date (date
// posted) and Effective Date (transaction date) columns, but the exported
// data only has the date posted.
self.log('scraping transactions');
var table = self.browser.query('#dbActivityGrid'),
headerCells = table.querySelectorAll('tr.header td'),
columnIndices = {},
dataRows = table.querySelectorAll('tr.item, tr.alternatingItem');
for (var i = 0; i < headerCells.length; i++) {
columnIndices[self.browser.text(headerCells[i])] = i;
}
var ok = true;
[
'Date',
'Effective Date',
// 'Check Number', // savings accounts don't have this column
'Description',
'Amount',
'Balance'
].forEach(function(columnName) {
if (ok && typeof columnIndices[columnName] == 'undefined') {
cb(new Error(
'Could not find column "' + columnName + '" on account page.'));
ok = false;
}
});
if (!ok) {
return;
}
for (var i = 0; i < dataRows.length; i++) {
cells = dataRows[i].querySelectorAll('td');
if (i == 0) {
if (dataRows[i].id == 'dbActivityGrid_EmptyTemplate') {
// No activity in the current date range for this account
break;
}
data.balances.fromList = self.getBalance(cells[columnIndices['Balance']]);
}
var transaction = {
datePosted : self.browser.text(cells[columnIndices['Date']]),
date : self.browser.text(cells[columnIndices['Effective Date']]),
description : self.browser.text(cells[columnIndices['Description']]),
amount : self.browser.text(cells[columnIndices['Amount']])
};
transaction.datePosted = moment(transaction.datePosted, 'MM/DD/YYYY');
transaction.date = moment(transaction.date , 'MM/DD/YYYY');
transaction.amount = utils.decimal(transaction.amount);
data.transactions.push(transaction);
if (typeof columnIndices['Check Number'] != 'undefined') {
var checkCell = cells[columnIndices['Check Number']],
checkLink = checkCell.querySelector('a[href^="/Accounts/CheckImageViewer.aspx"]');
if (checkLink) {
transaction.images = {};
checkLinks.push({
link : checkLink,
transaction : transaction
});
}
}
}
async.eachSeries(checkLinks, function(linkInfo, next) {
var checkNum = self.browser.text(linkInfo.link);
self.log('getting image for check #' + checkNum);
self.enableImages(function(request, response) {
if (response.statusCode == 200 &&
/\/Accounts\/CheckImageHandler\.ashx/.test(response.url)) {
linkInfo.transaction.images.check = 'data:'
+ response.headers['content-type'].split(';')[0]
+ ';base64,' + response.body.toString('base64');
}
}, function(disableImages) {
self.browser.click(linkInfo.link, function() {
if (!self.pageLoaded('check' + checkNum, cb)) return;
// the check page opens in a new window
self.browser.close(self.browser.window);
disableImages();
});
}, function() {
next();
});
}, function(err) {
self._processData(data, cb);
});
};
TVFCUSession.prototype._processData = function(data, cb) {
var self = this;
data.transactions.forEach(function(transaction) {
transaction.sourceId = self.makeTransactionId(transaction);
});
cb(null, data);
};
TVFCUSession.prototype.logout = function(cb) {
var self = this;
if (!cb) cb = function() { };
if (!self.loggedIn) {
self.log('may not be logged in');
}
self.log('visiting logout page');
self.browser.errors = [];
self.browser.visit(self.logoutUrl, function() {
if (!self.pageLoaded('logout', cb)) return;
var err = self.browser.error;
if (!err || err.message == "Unexpected token <") {
self.loggedIn = false;
self.log('logged out');
err = null;
}
cb(err);
});
};
module.exports = TVFCUSession;