UNPKG

banker

Version:

Downloads transactions and balances from online banking websites using Zombie.js.

347 lines (284 loc) 11.8 kB
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;