banker
Version:
Downloads transactions and balances from online banking websites using Zombie.js.
273 lines (229 loc) • 9.03 kB
JavaScript
var BankSession = require('./base'),
csv = require('csv'),
moment = require('moment'),
util = require('util'),
utils = require('../utils');
function PaypalSession(config) {
var self = this;
BankSession.call(self, config);
self.loginUrl = 'https://www.paypal.com/us/home';
self.accountUrl = 'https://www.paypal.com/us/cgi-bin/webscr?cmd=_account';
self.transactionsUrl = 'https://history.paypal.com/us/cgi-bin/webscr?cmd=_history';
self.logoutUrl = 'https://www.paypal.com/us/cgi-bin/webscr?cmd=_logout';
self.waitOptions = {
duration : 10000
};
self.waitOptions2 = {
duration : 40000
};
}
util.inherits(PaypalSession, BankSession);
PaypalSession.prototype.info = function() {
return {
description : 'PayPal (https://www.paypal.com/)',
configItems : {
bankName : '"paypal"',
email : 'PayPal email address (username)',
password : 'PayPal password',
welcomeText : 'The big welcome text after you log in. Either '
+ '"Welcome, Firstname Lastname" or "Hi again, Firstname!"'
},
accountItems : {
description : 'Account description (free-form, not used)'
}
};
};
PaypalSession.prototype.login = function(cb) {
var self = this;
if (self.loggedIn) {
self.log('already logged in');
cb(null);
return;
}
if (!self.config.welcomeText) {
cb(new Error(
'Fill in the welcomeText config property for this account.'));
}
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('#login_email' , self.config.email);
self.browser.fill('#login_password', self.config.password);
} catch (err) {
cb(err);
return;
}
self.browser.click('input[name=submit]');
self.browser.wait(self.waitOptions2, function() {
if (!self.pageLoaded('afterLogin0', cb)) return;
try {
var errorText = self.browser.text('div.alert.alert-warning');
if (errorText) {
cb(new Error(
'Error from PayPal: ' + errorText));
return;
}
} catch (err) { }
self.log('visiting account page');
self.browser.visit(self.accountUrl, function() {
if (!self.pageLoaded('account', cb)) return;
var welcomeText = null;
try {
welcomeText = self.browser.text('#headline > h2');
} catch (err) { }
if (!welcomeText) {
try {
welcomeText = self.browser.text('p.nemo_welcomeMessageHeader');
self._usingNewPaypal = true;
} catch (err) { }
}
if (welcomeText != self.config.welcomeText) {
cb(new Error(
'Expected welcome text to equal "' + self.config.welcomeText
+ '" but found "' + welcomeText + '".'));
return;
}
self._loggedIn(cb);
});
});
});
};
PaypalSession.prototype._loggedIn = function(cb) {
var self = this;
self.loggedIn = true;
self.log('logged in');
cb(null);
};
PaypalSession.prototype.getTransactions = function(account, cb) {
var self = this;
var data = {
transactions : [],
balances : {}
};
self.log('visiting account page');
self.browser.visit(self.accountUrl, function() {
if (!self.pageLoaded('account', cb)) return;
try {
if (self._usingNewPaypal) {
data.balances.actual = utils.decimal(
self.browser.text(
'.balanceModule .balanceNumeral .large'));
} else {
data.balances.actual = utils.decimal(
self.browser.text(
'li.balance span.balance strong').split(' ')[0]);
}
} catch (err) {
cb(err);
return;
}
self.log('visiting transaction history page');
self.browser.visit(self.transactionsUrl, function() {
if (!self.pageLoaded('transactions', cb)) return;
if (account.minDate || account.maxDate) {
var minDateSel = '#from_date',
maxDateSel = '#to_date';
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('#show');
self.browser.wait(self.waitOptions, function() {
if (!self.pageLoaded('accountExpanded', cb)) return;
self._scrapeTransactions(data, cb);
});
} else {
self._scrapeTransactions(data, cb);
}
});
});
};
PaypalSession.prototype._scrapeTransactions = function(data, cb) {
var self = this;
self.log('downloading transactions as CSV');
var csvButton = null;
try {
csvButton = self.browser.querySelector('input[name=download_file_submit]');
} catch (err) { }
if (!csvButton) {
cb(new Error(
'Could not find CSV download button.'));
return;
}
self.browser.click(csvButton);
self.browser.wait(self.waitOptions2, function() {
if (!self.pageLoaded('csv', cb)) return;
var csvData = self.browser.response.body;
// Change headers from "Field1, Field2, " to "Field1,Field2"
csvData = csvData.split('\n');
csvData[0] = csvData[0]
.replace(/[,\s]+$/, '')
.split(',')
.map(function(fieldName) { return fieldName.trim(); })
.join(',');
csvData = csvData.join('\n');
csv.parse(csvData, { columns : true }, function(err, rows) {
data.balances.fromList = utils.decimal(rows[0]['Balance']);
rows.forEach(function(row) {
switch (row['Type']) {
case 'Invoice Sent':
case 'Update to eCheck Received':
case 'Cancelled Fee':
// These are not real transactions; ignore them
break;
default:
if (row['Status'] != 'Uncleared' && row['Status'] != 'Pending') {
var transaction = {
date : moment(row['Date'], 'MM/DD/YYYY'),
description : row['Type'] + ' (' + row['Name'] + ')',
memo : row['Status'],
sourceId : row['Transaction ID'],
// Non-standard fields follow
fromName : row['Name'],
grossAmount : utils.decimal(row['Gross']),
feeAmount : utils.decimal(row['Fee']),
netAmount : utils.decimal(row['Net'])
};
transaction.amount = transaction.netAmount;
var from = row['From Email Address'].trim();
if (from) {
transaction.fromEmail = from; // non-standard field
transaction.memo += ' from ' + from;
}
data.transactions.push(transaction);
}
break;
}
});
});
cb(null, data);
});
};
PaypalSession.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 == "Cannot call method 'indexOf' of null") {
self.loggedIn = false;
self.log('logged out');
err = null;
}
cb(err);
});
};
module.exports = PaypalSession;