Martin Donnelly 57bd6c8e6a init
2018-06-24 21:15:03 +01:00

894 lines
29 KiB
JavaScript

/**
* Controller to manage the accounts functions
*/
'use strict';
var _ = require('lodash');
var Q = require('q');
var httpStatus = require('http-status-codes');
var mongodb = require('mongodb');
var debug = require('debug')('webconsole-api:controllers:accounts');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var swaggerUtils = require(global.pathPrefix + '../utils/swaggerUtils.js');
var anon = require(global.pathPrefix + '../utils/anon.js');
var referenceUtils = require(global.pathPrefix + '../utils/references.js');
var acquirerUtils = require(global.pathPrefix + '../utils/acquirers/acquirer.js');
var responsesUtils = require(global.pathPrefix + '../utils/responses.js');
const deleteAccountImpl = require(global.pathPrefix + '../impl/delete_account.js');
module.exports = {
getAccounts: getAccounts,
getAccount: getAccount,
updateAccount: updateAccount,
deleteAccount: deleteAccount,
addAccountCredorax: addAccountCredorax,
addAccountWorldpay: addAccountWorldpay,
addAccountDemo: addAccountDemo
};
/**
* Get the account history
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function getAccounts(req, res) {
//
// Get the query params from the request and the session
//
var clientID = req.session.data.clientID;
var isMerchant = req.session.data.isMerchant;
var limit = req.swagger.params.limit.value;
var skip = req.swagger.params.skip.value;
var minDate = req.swagger.params.minDate.value;
var maxDate = req.swagger.params.maxDate.value;
var includeDeleted = req.swagger.params.includeDeleted.value;
var query = {
ClientID: clientID
};
//
// Exclude deleted items unless we have been asked to keep them
//
if (!includeDeleted) {
// Ignore items with the AccountDelete or API created bits set
// jshint -W016
query.AccountStatus = {
$bitsAllClear: utils.AccountDeleted | utils.AccountApiCreated
};
// jshint +W016
}
//
// Add date limits if included
//
if (minDate || maxDate) {
query.LastUpdate = {};
if (minDate) {
query.LastUpdate.$gte = minDate;
}
if (maxDate) {
query.LastUpdate.$lte = maxDate;
}
}
//
// If the user is not a merchant then prevent listing any merchant accounts
//
if (!isMerchant) {
query.AccountType = {$ne: 'Credit/Debit Receiving Account'};
}
//
// Define the projection based on the Swagger definition
//
var projection = swaggerUtils.swaggerToMongoProjection(
req.swagger.operation,
true // include _id so we know how to select an individual device.
);
//
// Make the query. Note limit & skip have defaults defined in the
// swagger definition, so will always exist even if not requested
//
mainDB.collectionAccount.find(query, projection)
.skip(skip)
.limit(limit)
.sort({LastUpdate: -1}) // Hard-coded reverse sort by time
.toArray(function(err, items) {
if (err) {
debug('- failed to getAccounts', err);
res.status(httpStatus.BAD_GATEWAY).json({
code: 121,
info: 'Database offline'
});
} else {
//
// Anonymise all the accounts before sending them out
//
_.forEach(
items,
function(value, index, collection) {
//
// Anonymise all the account before sending them out
//
anon.anonymiseAccount(value);
//
// Rename _id to AccountId
//
value.AccountID = value._id;
delete value._id;
});
//
// Null any nullable fields
//
swaggerUtils.getAndApplyNullableFields(req.swagger.operation, items);
res.status(httpStatus.OK).json(items);
}
});
}
/**
* Gets the account details for a specific account.
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function getAccount(req, res) {
//
// Get the query params from the request and the session
//
var clientID = req.session.data.clientID;
var isMerchant = req.session.data.isMerchant;
var accountId = req.swagger.params.objectId.value;
//
// Build the query. The limits are:
// - Must match the id of the item we are looking for
// - Current user must be the account owner (to protect against Insecure
// Direct Object References).
//
var query = {
_id: mongodb.ObjectID(accountId),
ClientID: clientID
};
//
// If the user is not a merchant then prevent listing any merchant accounts
//
if (!isMerchant) {
query.AccountType = {$ne: 'Credit/Debit Receiving Account'};
}
//
// Define the fields based on the Swagger definition.
//
var projection = swaggerUtils.swaggerToMongoProjection(
req.swagger.operation,
true
);
//
// Build the options to encapsulate the projection
//
var options = {
fields: projection,
comment: 'WebConsole:getAccount' // For profiler logs use
};
//
// Make the request
//
mainDB.findOneObject(mainDB.collectionAccount, query, options, false,
function(err, item) {
if (err) {
debug('- failed to getAccount', err);
res.status(httpStatus.BAD_GATEWAY).json({
code: 197,
info: 'Database offline'
});
} else if (item === null) {
//
// Nothing found
//
res.status(httpStatus.NOT_FOUND).json({
code: 192,
info: 'Not found'
});
} else {
//
// Anonymise all the account before sending them out
//
anon.anonymiseAccount(item);
//
// Rename _id to AccountId
//
item.AccountID = item._id;
delete item._id;
//
// Null any nullable fields
//
swaggerUtils.getAndApplyNullableFields(req.swagger.operation, item);
res.status(httpStatus.OK).json(item);
}
});
}
/**
* Updates billing address or customer name for a specific account.
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function updateAccount(req, res) {
//
// Get the query params from the request and the session
//
var clientID = req.session.data.clientID;
var accountId = req.swagger.params.objectId.value;
//
// Get the optional parameters for the update
//
var accountName = req.swagger.params.body.value.ClientAccountName;
var billingAddress = req.swagger.params.body.value.BillingAddress;
var lock = req.swagger.params.body.value.Lock;
if (accountName === undefined &&
billingAddress === undefined &&
lock === undefined) {
//
// Nothing to update
//
res.status(httpStatus.BAD_REQUEST).json({
code: 30004,
info: 'No update parameters included'
});
return;
}
//
// If we have been given a new billing address we need to check it actually
// belongs to the user. This requires a database lookup so we use a
// promise to wait for the result.
//
var validAddressPromise;
const FAILED_UPDATE = 'BRIDGE: FAILED UPDATE';
const NULL_ADDRESS = 'BRIDGE: ADDR CANT BE NULL';
if (billingAddress) {
validAddressPromise = referenceUtils.isValidAddressRef(
clientID,
billingAddress,
'WebConsole:updateAccount'
);
} else {
//
// Haven't asked to update billing address, so nothing to check
//
validAddressPromise = Q.resolve();
}
//
// Also check that we don't have an account with the same name already
//
var validAccountPromise = Q.resolve();
const SAME_NAME = 'Bridge: Account with the same name';
if (accountName) {
var options = {
fields: {
_id: true
},
comment: 'WebConsole:updateAccount'
};
var uniqueNameQuery = {
ClientID: clientID,
ClientAccountName: accountName
};
validAccountPromise = Q.nfcall(
mainDB.findOneObject,
mainDB.collectionAccount,
uniqueNameQuery,
options,
false
).then(function(result) {
if (result !== null && result._id.toString() !== accountId) {
// Some other account has this name
return Q.reject({name: SAME_NAME});
}
return Q.resolve();
});
}
//
// Update the account, after checking the billing address is valid
//
var updatePromise = Q.all([validAddressPromise, validAccountPromise]).then(function() {
var query = {
_id: mongodb.ObjectID(accountId), // The account to update
ClientID: clientID, // Must be *my* account
AccountStatus: {
$bitsAllClear: utils.AccountDeleted // Must not be "deleted"
}
};
var updates = {
$set: {
LastUpdate: new Date()
},
$inc: {
LastVersion: 1
}
};
if (accountName !== undefined) {
updates.$set.ClientAccountName = accountName;
}
if (billingAddress !== undefined) {
//
// Special case: can't set the billing address to NULL (but this
// isn't checked by validation because we could *return* a null
// billing address for legacy accunts.
//
if (billingAddress === null) {
return Q.reject({name: NULL_ADDRESS});
}
updates.$set.BillingAddress = billingAddress;
}
//
// Update the locked status of the account
// jshint -W016
//
if (lock !== undefined) {
if (lock) {
updates.$bit = {
AccountStatus: {or: utils.AccountLocked}
};
} else {
updates.$bit = {
AccountStatus: {and: ~utils.AccountLocked}
};
}
}
// jshint +W016
var options = {
upsert: false,
multi: false
};
return Q.nfcall(
mainDB.updateObject,
mainDB.collectionAccount,
query,
updates,
options,
false
).then(function(results) {
if (results.result.n === 0) {
return Q.reject({name: FAILED_UPDATE});
} else {
return Q.resolve();
}
});
});
//
// Run all the promises and check they pass
//
Q.all([validAddressPromise, validAccountPromise, updatePromise])
.then(function() {
// All good
res.status(200).json();
})
.catch(function(error) {
debug('-- error updating account: ', error);
if (
error &&
error.hasOwnProperty('name')
) {
switch (error.name) {
case referenceUtils.ERRORS.INVALID_ADDRESS:
// Billing address is not valid
res.status(httpStatus.NOT_FOUND).json({
code: 393,
info: 'Billing address not found'
});
break;
case NULL_ADDRESS:
// Billing address is not valid
res.status(httpStatus.BAD_REQUEST).json({
code: 393,
info: 'Billing address can\'t be null'
});
break;
case FAILED_UPDATE:
// Couldn't update - probably not *my* account
res.status(httpStatus.NOT_FOUND).json({
code: 395,
info: 'Account not found'
});
break;
case SAME_NAME:
// Can't have an account with the same name
res.status(httpStatus.CONFLICT).json({
code: 30107,
info: 'Account with the same name already exists'
});
break;
case 'MongoError':
// Mongo Error
res.status(httpStatus.BAD_GATEWAY).json({
code: 392,
info: 'Database Unavailable'
});
break;
default:
//
// Unknown error
//
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
code: -1,
info: 'Unexpected error'
});
break;
}
} else {
//
// Unknown error
//
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
code: -1,
info: 'Unexpected error'
});
}
})
.done(); // Catch all
}
/**
* Base function to adds a new merchant account. All merchant accounts are added
* in the same basic way, but may have different requirements for the format
* of parameters (particualarly MerchantID and ClientKey). Any validation
* of format, or liveness of account, should be done before calling this function,
* which assumes everything is correct.
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Object} extData - additional info to set in the account
* @param {String} extData.VendorAccountName - name of account at aquirer
* @param {String} extData.AcquirerName - Name of acquirer (e.g. "Demo")
* @param {String} extData.VendorID - ID acquirer in our system (e.g. "Bridge")
* @param {String} extData.IconLocation - Name of the icon for this acquirer
*
* @return {Promise} - A promise for the result of adding the account
*/
function addAccountBase(req, res, extData) {
//
// Get the query params from the request and the session
//
var clientID = req.session.data.clientID;
var isMerchant = req.session.data.isMerchant;
//
// Precondition 0: Must be able to encrypt the account ID and cipher
//
let encryptP = Q.resolve();
let encMerchantID = '';
let encCipher = '';
const ENCRYPTION_FAILED = 'BRIDGE: Encryption failed';
if (extData.VendorID !== 'Bridge') {
encMerchantID = utils.encryptDataV1(req.swagger.params.body.value.AcquirerMerchantID);
encCipher = utils.encryptDataV1(req.swagger.params.body.value.AcquirerCipher);
if (!_.isString(encMerchantID) || !_.isString(encCipher)) {
encryptP = Q.reject({name: ENCRYPTION_FAILED});
}
}
//
// Merge the default blank account plus the parameters from the request
//
var newAccount = _.assign(
mainDB.blankAccount(),
{
// Base items
AccountType: 'Credit/Debit Receiving Account',
ReceivingAccount: 1,
PaymentsAccount: 0,
AcquirerName: extData.AcquirerName,
VendorID: extData.VendorID,
IconLocation: extData.IconLocation,
UserImage: 'CompanyLogo0',
AccountStatus: utils.AccountLocked, // Prevents the account being deleted
VendorAccountName: extData.VendorAccountName,
// Items from the request
NameOnAccount: req.swagger.params.body.value.NameOnAccount,
ClientAccountName: req.swagger.params.body.value.ClientAccountName,
BillingAddress: req.swagger.params.body.value.BillingAddress,
AcquirerMerchantID: encMerchantID,
AcquirerCipher: encCipher,
// Calculated items
ClientID: clientID,
LastUpdate: new Date(),
LastVersion: 1
});
//
// Precondition 1: Must be a merchant
//
const NOT_A_MERCHANT = 'BRIDGE: Client is not a registered merchant';
const isAMerchantP = isMerchant ? Q.resolve() : Q.reject({name: NOT_A_MERCHANT});
//
// Precondition 2: Merchant must be configured (have CompanyName and
// CompanyAlias).
//
var merchantConfigQuery = {
ClientID: clientID,
'Merchant.0.CompanyName': {$ne: ''},
'Merchant.0.CompanyAlias': {$ne: ''}
};
var options = {
fields: {}, // Don't want any fields, just checking existence
comment: 'WebConsole:addAccountCredorax'
};
const MERCHANT_NOT_CONFIGURED = 'Bridge: Merchant is not configured';
var merchantConfiguredP = isAMerchantP.then(() => {
return Q.nfcall(
mainDB.findOneObject,
mainDB.collectionClient,
merchantConfigQuery,
options,
false
).then(function(result) {
if (!result) {
return Q.reject({name: MERCHANT_NOT_CONFIGURED});
}
return Q.resolve();
});
});
//
// Precondition 3: Client must have a device registered before
//
var deviceQuery = {
ClientID: clientID,
DeviceStatus: {
$bitsAllSet: utils.DeviceFullyRegistered
}
};
const NO_DEVICE = 'Bridge: No devices on account';
var hasDeviceP = merchantConfiguredP.then(function() {
return Q.nfcall(
mainDB.findOneObject,
mainDB.collectionDevice,
deviceQuery,
options,
false).then(function(result) {
if (!result) {
return Q.reject({name: NO_DEVICE});
}
return Q.resolve();
});
});
//
// Precondition 4: Must not have an active account with the same name already
//
const SAME_NAME = 'Bridge: Account with the same name';
var uniqueNameQuery = {
ClientID: clientID,
ClientAccountName: newAccount.ClientAccountName,
AccountStatus: {
$bitsAllClear: utils.AccountDeleted
}
};
var checkUniqueNameP = hasDeviceP.then(function() {
return Q.nfcall(
mainDB.findOneObject,
mainDB.collectionAccount,
uniqueNameQuery,
options,
false
).then(function(result) {
if (result !== null) {
return Q.reject({name: SAME_NAME});
}
return Q.resolve();
});
});
//
// Precondition 5: Address must exist and belong to me
//
var addressExistsP = checkUniqueNameP.then(function() {
return referenceUtils.isValidAddressRef(
clientID,
newAccount.BillingAddress,
'WebConsole:addAccountBase'
);
});
//
// Precondition 6: Account must be validated with the acquirer
//
var validateP = addressExistsP.then(
() => acquirerUtils.validateMerchantAccount(newAccount)
);
//
// Add the account details to the database
//
var addP = validateP.then(function() {
return Q.nfcall(
mainDB.addObject,
mainDB.collectionAccount,
newAccount,
undefined,
false
);
});
//
// Run all the promises and return the result
//
return Q.all([encryptP, isAMerchantP, merchantConfiguredP, hasDeviceP, checkUniqueNameP, addressExistsP, validateP, addP])
.then(function(results) {
var newAccountResult = results[7][0];
res.status(201).json({
id: newAccountResult._id
});
return;
})
.catch(function(error) {
debug('-- error adding account: ', error);
const responses = [
[
'MongoError',
// Mongo Error
httpStatus.BAD_GATEWAY, 30104, 'Database Unavailable',
true
],
[
referenceUtils.ERRORS.INVALID_ADDRESS,
httpStatus.NOT_FOUND, 30102, 'Billing address not found',
true
],
[
ENCRYPTION_FAILED,
httpStatus.BAD_REQUEST, -1, 'Invalid AcquirerMerchantID or AcquirerCipher',
true
],
[
NOT_A_MERCHANT,
httpStatus.FORBIDDEN, 30101, 'Client is not a merchant',
true
],
[
MERCHANT_NOT_CONFIGURED,
httpStatus.PRECONDITION_FAILED, 30106,
'Company details must be configured before adding a merchant account',
true
],
[
SAME_NAME,
httpStatus.CONFLICT, 30103, 'An account with the same description already exists',
true
],
[
NO_DEVICE,
httpStatus.PRECONDITION_FAILED, 30105,
'Client must have a registered device before adding a merchant account',
true
],
//
// Errors from the acquirer validation
//
[
acquirerUtils.ERRORS.UNKNOWN_ACQUIRER,
httpStatus.BAD_REQUEST, 30109, 'Merchant acquirer unknown',
true
],
[
acquirerUtils.ERRORS.ACQUIRER_DOWN,
httpStatus.BAD_GATEWAY, 30110, 'Cannot connect to acquirer',
true
],
[
acquirerUtils.ERRORS.INVALID_MERCHANT_ACCOUNT_DETAILS,
httpStatus.BAD_REQUEST, 30111, 'Receiving account information unreadable',
true
],
[
acquirerUtils.ERRORS.ACQUIRER_UNKNOWN_ERROR,
httpStatus.BAD_GATEWAY, 30112, 'Unknown acquirer error',
true
],
[
acquirerUtils.ERRORS.ACQUIRER_BAD_REQUEST,
httpStatus.INTERNAL_SERVER_ERROR, 30113, 'Bad request to acquirer',
true
],
[
acquirerUtils.ERRORS.ACQUIRER_UNAUTHORIZED,
httpStatus.BAD_REQUEST, 30114, 'Merchant account details invalid.',
true
],
[
acquirerUtils.ERRORS.ACQUIRER_MERCHANT_DISABLED,
httpStatus.BAD_REQUEST, 30115, 'Merchant account disabled. Re-enable before adding',
true
],
[
acquirerUtils.ERRORS.ACQUIRER_INTERNAL_SERVER_ERROR,
httpStatus.BAD_GATEWAY, 30116, 'Internal server error at enquirer',
true
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respond(res, error);
})
.done();
}
/**
* Adds a new Credorax merchant account. This can only be done for clients who
* have been enabled as merchants, and do not already have the max number of
* accounts
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function addAccountCredorax(req, res) {
//
// TODO: Validate account against Credorax
//
//
// Call the base function to add it to the database
//
return addAccountBase(req, res, {
VendorAccountName: 'Merchant Card Account', // Not used by Credorax so hardcode
AcquirerName: 'Credorax',
VendorID: 'Credorax',
IconLocation: 'credorax-account.png'
});
}
/**
* Adds a new Worldpay merchant account. This can only be done for clients who
* have been enabled as merchants, and do not already have the max number of
* accounts
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function addAccountWorldpay(req, res) {
//
// TODO: Validate account against Worldpay
//
//
// Call the base function to add it to the database
//
return addAccountBase(req, res, {
VendorAccountName: 'Merchant Card Account', // Not used by worldpay so hardcode
AcquirerName: 'Worldpay',
VendorID: 'Worldpay',
IconLocation: 'worldpay-account.png'
});
}
/**
* Adds a new Demo merchant account. This can only be done for clients who
* have been enabled as merchants, and do not already have the max number of
* accounts.
* This is an internal demo account that will never perform a real charge against
* a card.
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
function addAccountDemo(req, res) {
//
// Add (Demo) to the client account name, to help make it more obvious
//
req.swagger.params.body.value.ClientAccountName += ' (Demo)';
//
// Call the base function to add it to the database
//
return addAccountBase(req, res, {
VendorAccountName: 'Standard Merchant Account', // Not used, so hardcode
AcquirerName: 'Demo',
VendorID: 'Bridge',
IconLocation: 'BRIDGE_MERCHANT.png'
});
}
/**
* Deletes an account from the system by setting the "deleted" status. It also
* attempts to disable the token on the merchant aquirer system if appropriate.
*
* @param {Object} req - Express request object.
* @param {Object} res - Express response object.
*/
function deleteAccount(req, res) {
//
// Get the query params from the request and the session
//
const clientID = req.session.data.clientID;
const accountId = req.swagger.params.objectId.value;
//
// Call the implementation and return the properly formatted
//
return deleteAccountImpl.deleteAccount(clientID, accountId)
.then(function() {
return res.status(200).json();
})
.catch(function(error) {
debug('-- error deleting account: ', error);
const responses = [
[
deleteAccountImpl.ERRORS.RELATED_INVOICES,
httpStatus.CONFLICT, 30108, 'Account can\'t be deleted while related active invoices exist', true
],
[
deleteAccountImpl.ERRORS.NOT_FOUND,
// AccountID is not valid (or doesn't belong to *me*)
httpStatus.NOT_FOUND, 153, 'Account not found', true
],
[
deleteAccountImpl.ERRORS.FAILED_UPDATE,
// AccountID is not valid (or doesn't belong to *me*)
httpStatus.NOT_FOUND, 153, 'Account not found', true
],
[
deleteAccountImpl.ERRORS.LOCKED,
httpStatus.FORBIDDEN, 243, 'Account is locked and cant be deleted', true
],
[
acquirerUtils.ERRORS.UNKNOWN_ACQUIRER,
httpStatus.INTERNAL_SERVER_ERROR, 241, 'Unknown acquirer', true
],
[
acquirerUtils.ERRORS.ACQUIRER_DOWN,
httpStatus.BAD_GATEWAY, 244, 'Cannot connect to acquiring bank', true
],
[
'MongoError',
// Mongo Error
httpStatus.BAD_GATEWAY, 30104, 'Database Unavailable'
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
return responseHandler.respond(res, error);
})
.done();
}