bridge-node-server/node_server/integration_api/controllers/payments_controller.js

785 lines
26 KiB
JavaScript
Raw Normal View History

2018-06-24 20:15:03 +00:00
/* eslint-disable */
/**
* @fileOverview Controllers for functions related to payments
*/
'use strict';
const _ = require('lodash');
const Q = require('q');
const debug = require('debug')('integration-api:clients');
const httpStatus = require('http-status-codes');
const config = require(global.configFile);
const mainDB = require(global.pathPrefix + 'mainDB.js');
const utils = require(global.pathPrefix + 'utils.js');
const references = require(global.pathPrefix + '../utils/references.js');
const anon = require(global.pathPrefix + '../utils/anon.js');
const impl = require(global.pathPrefix + '../impl/confirm_transaction.js');
const acquirers = require(global.pathPrefix + '../utils/acquirers/acquirer.js');
const acqErrors = require(global.pathPrefix + '../utils/acquirers/acquirer_errors.js');
const responsesUtils = require(global.pathPrefix + '../utils/responses.js');
const implRedeem = require(global.pathPrefix + '../impl/redeem_paycode.js');
const implGetUpdate = require(global.pathPrefix + '../impl/get_transaction_update.js');
const promClient = require('prom-client');
const counters = {
takePayment: new promClient.Counter({
name: 'bridge_server_intapi_takepayment_total',
help: 'Count of calls to takePayment in the integrations API.',
labelNames: ['result']
})
};
module.exports = {
takePayment,
redeemPaycode,
getTransactionUpdate
};
const CLIENT_NOT_OWNED = 'BRIDGE: Client not owned by this merchant';
const FAILED_ADD_ADDRESS = 'BRIDGE: Failed to add billing address';
const DB_ERROR_ADD_ADDRESS = 'BRIDGE: DB failed when adding billing address';
const FAILED_ADD_ACCOUNT = 'BRIDGE: Failed to add account';
const DB_ERROR_ADD_ACCOUNT = 'BRIDGE: DB failed when adding account';
const FAILED_ADD_TRANSACTION = 'BRIDGE: Failed to add transaction';
const DB_ERROR_ADD_TRANSACTION = 'BRIDGE: DB failed when adding transaction';
/**
* Handler for the takePayment function.
* This processes a direct card payment
*
* @param {Object} req - the request object
* @param {Object} res - the response object
*/
function takePayment(req, res) {
//
// To take a direct payment we need to:
// 1. Check the client was added by the merchant (for security)
// 2. Add the billing Address if different from residential address
// 3. Create a client Account (NOT storing encrypted card PAN)
// 4. Create a Transaction for the payment
// 5. Process payment (passing in decrypted details rather than getting from account)
//
const body = req.swagger.params.body.value;
const merchant = req.session.data.Merchant;
const sessionToken = req.session.data.PseudoSession;
//
// 1. Find the client, ensuring they were added by this merchant
//
const clientP = findClient(body.email, merchant);
//
// 2. Add the billing address
//
const addressP = clientP.then((client) => addAddress(client, body.cardDetails.BillingAddress));
//
// 3. Add the client account
//
const accountP = Q.all([clientP, addressP])
.spread((client, address) => addAccount(client, address, body.cardDetails));
//
// 4. Add a transaction
//
const transactionP = Q.all([clientP, accountP])
.spread((client, account) => addTransaction(client, account, merchant, body, sessionToken));
//
// 5. Process the transactions
//
const resultP = Q.all([clientP, transactionP])
.spread((client, transaction) => makePayment(client, transaction, body));
//
// Response handling
//
Q.all([clientP, addressP, accountP, transactionP, resultP]).then((results) => {
res.status(httpStatus.OK).json({
TransactionID: results[3]._id.toString()
});
counters.takePayment.inc({result: 'success'}, 1, new Date());
}).catch((error) => {
debug('Error:', error);
//
// Define the responses
//
const responses = [
//
// Errors when reading from the database
//
[
'MongoError',
httpStatus.INTERNAL_SERVER_ERROR, 510, 'Database Offline', true
],
//
// Errors from adding database entries needed for main processing
//
[
CLIENT_NOT_OWNED,
httpStatus.FORBIDDEN, 999, 'Client not owned by this merchant'
],
[
FAILED_ADD_ADDRESS,
httpStatus.INTERNAL_SERVER_ERROR, 999, 'Failed to add billing address'
],
[
DB_ERROR_ADD_ADDRESS,
httpStatus.BAD_GATEWAY, 999, 'DB failed when adding billing address'
],
[
FAILED_ADD_ACCOUNT,
httpStatus.INTERNAL_SERVER_ERROR, 999, 'Failed to add account'
],
[
DB_ERROR_ADD_ACCOUNT,
httpStatus.BAD_GATEWAY, 999, 'DB failed when adding account'
],
[
FAILED_ADD_TRANSACTION,
httpStatus.INTERNAL_SERVER_ERROR, 999, 'Failed to add transaction'
],
[
DB_ERROR_ADD_TRANSACTION,
httpStatus.BAD_GATEWAY, 999, 'DB failed when adding transaction'
],
//
// Errors from the main implementation
//
[
impl.ERRORS.MERCHANT_NOT_FOUND,
httpStatus.FORBIDDEN, 551, 'Merchant information not found'
],
[
impl.ERRORS.CLIENT_DETAILS_NOT_SET,
httpStatus.FORBIDDEN, 552, 'User details not set'
],
[
impl.ERRORS.MERCHANT_DETAILS_NOT_SET,
httpStatus.FORBIDDEN, 553, 'Merchant details not set'
],
[
impl.ERRORS.CLIENT_KYC_INCOMPLETE,
httpStatus.FORBIDDEN, 554, 'Additional customer information required'
],
[
impl.ERRORS.MERCHANT_KYC_INCOMPLETE,
httpStatus.FORBIDDEN, 555, 'Additional merchant information required'
],
[
impl.ERRORS.TRANSACTION_TOTAL_TOO_HIGH,
httpStatus.BAD_REQUEST, 310, 'Total above current limit'
],
[
impl.ERRORS.TRANSACTION_TOTAL_TOO_LOW,
httpStatus.BAD_REQUEST, 311, 'Total below current limit'
],
[
impl.ERRORS.FAILED_SET_CONFIRMED,
httpStatus.BAD_GATEWAY, 510, 'Database offline'
],
[
impl.ERRORS.FAILED_SET_COMPLETE,
httpStatus.BAD_GATEWAY, 506, 'Database offline'
],
[
impl.ERRORS.FAILED_ADD_HISTORY,
httpStatus.BAD_GATEWAY, 507, 'Database offline'
],
[
impl.ERRORS.FAILED_UPDATE_CUSTOMER_BALANCE,
httpStatus.BAD_GATEWAY, 508, 'Database offline'
],
[
impl.ERRORS.FAILED_UPDATE_MERCHANT_BALANCE,
httpStatus.BAD_GATEWAY, 509, 'Database offline'
],
[
impl.ERRORS.MERCHANT_ACCOUNT_NOT_FOUND,
httpStatus.BAD_REQUEST, 497, 'Invalid Merchant AccountID'
],
[
impl.ERRORS.CUSTOMER_ACCOUNT_NOT_FOUND,
httpStatus.INTERNAL_SERVER_ERROR, 494, 'Invalid Customer AccountID'
],
[
impl.ERRORS.MERCHANT_ACCOUNT_NOT_RECEIVING,
httpStatus.BAD_REQUEST, 498, 'Not a receiving account'
],
[
impl.ERRORS.CUSTOMER_ACCOUNT_NOT_PAYMENTS,
httpStatus.INTERNAL_SERVER_ERROR, 495, 'Not a payments account'
],
//
// Errors from the acquirer
//
[
acqErrors.UNKNOWN_ACQUIRER,
httpStatus.BAD_REQUEST, 532, 'Merchant acquirer unknown',
true
],
[
acqErrors.INVALID_COMBINATION,
httpStatus.BAD_REQUEST, 536, 'Invalid payment type',
true
],
[
acqErrors.ACQUIRER_DOWN,
httpStatus.BAD_GATEWAY, 533, 'Cannot connect to acquirer',
true
],
[
acqErrors.INVALID_MERCHANT_NAME,
httpStatus.FORBIDDEN, 534, 'Invalid Merchant account details.',
true
],
[
acqErrors.INVALID_MERCHANT_ACCOUNT_DETAILS,
httpStatus.INTERNAL_SERVER_ERROR, 535, 'Receiving account information unreadable',
true
],
[
acqErrors.INVALID_CARD_DETAILS,
httpStatus.INTERNAL_SERVER_ERROR, 536, 'Payment account information unreadable',
true
],
[
acqErrors.ACQUIRER_UNKNOWN_ERROR,
httpStatus.INTERNAL_SERVER_ERROR, 537, 'Error processing payment',
true
],
[
acqErrors.ACQUIRER_BAD_REQUEST,
httpStatus.INTERNAL_SERVER_ERROR, 538, 'Error processing payment',
true
],
[
acqErrors.ACQUIRER_INVALID_PAYMENT_DETAILS,
httpStatus.BAD_REQUEST, 540, 'Invalid payment details',
true
],
[
acqErrors.ACQUIRER_UNAUTHORIZED,
httpStatus.BAD_REQUEST, 541, 'Merchant account unauthorized with acquirer',
true
],
[
acqErrors.ACQUIRER_MERCHANT_DISABLED,
httpStatus.BAD_REQUEST, 542, 'Merchant account disabled with acquirer',
true
],
[
acqErrors.ACQUIRER_INTERNAL_SERVER_ERROR,
httpStatus.BAD_GATEWAY, 543, 'Error processing payment',
true
],
[
acqErrors.CARD_EXPIRED,
httpStatus.FORBIDDEN, 544, 'Card has expired',
true
],
[
acqErrors.PAYMENT_FAILED_UNSPECIFIED,
httpStatus.BAD_REQUEST, 545, 'Unspecified error',
true
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respond(res, error);
counters.takePayment.inc({result: 'fail'}, 1, new Date());
});
}
/**
* Find the appropriate client and ensure that they have been added by this merchant.
* If we don't find the client at all, we still respond with CLIENT_NOT_OWNED to
* avoid leaking anything about whether the email address existing in the service or not.
*
* @param {String} email - the email address of the client
* @param {Object} merchant - the merchant object
* @returns {Promise} - Promise for the client
*/
function findClient(email, merchant) {
return references.getClientByEmail(email)
.then((client) => {
if (client.OperatorName !== merchant.ClientID) {
return Q.reject(CLIENT_NOT_OWNED);
}
return client;
})
.catch((err) => Q.reject(CLIENT_NOT_OWNED));
}
/**
* Add the billing address. We set the name to "Billing Address" plus a random
* string to ensure that the name is unique.
*
* @param {Object} client - the client to add the address for
* @param {Object} addressInfo - the billing address info from the request
*
* @return {Promise} - a promise for the added address
*/
function addAddress(client, addressInfo) {
const address = _.clone(addressInfo);
_.defaults(
address,
{
ClientID: client.ClientID,
AddressDescription: 'Billing address ' + utils.timeBasedRandomCode(),
DateAdded: new Date(),
LastUpdate: new Date()
},
mainDB.blankAddress()
);
const addP = Q.nfcall(
mainDB.addObject,
mainDB.collectionAddresses,
address,
{},
true
);
return addP
.then((objects) => {
if (objects.length === 0) {
return Q.reject(FAILED_ADD_ADDRESS);
} else {
return objects[0];
}
})
.catch((err) => Q.reject(DB_ERROR_ADD_ADDRESS));
}
/**
* Adds an account to the database for the transaction that is about to be made.
* Note that we DO NOT store and actual card details as we can't encrypt them
* (as we don't have the device key to do so).
*
* @param {Object} client - the client object
* @param {Object} address - the billing address that was just added
* @param {Object} cardDetails - card details from the request
* @returns {Promise} - promise for the added account
*/
function addAccount(client, address, cardDetails) {
//
// Build the account structure
//
const account = _.defaults(
{
ClientID: client.ClientID,
BillingAddress: address._id.toString(),
NameOnAccount: cardDetails.NameOnAccount,
CardPAN: anon.anonymiseCardPAN(cardDetails.CardPAN),
ClientAccountName: 'Payment details ' + utils.timeBasedRandomCode(),
AccountType: 'Direct Credit/Debit Card Payment', // Custom type for these transaction types
ReceivingAccount: 0,
PaymentsAccount: 1,
/* jshint -W016 */
AccountStatus: utils.AccountLocked | utils.AccountApiCreated,
/* jshint +W016 */
LastUpdate: new Date()
},
mainDB.blankAccount()
);
//
// Tokenise the card with Worldpay to get further details.
// Need to add in the unencrypted card details so we can tokenise them
//
const tokeniseDetails = {
NameOnAccount: cardDetails.NameOnAccount,
CardPAN: cardDetails.CardPAN,
CVV: cardDetails.CardCVV,
CardExpiry: cardDetails.ExpiryDate,
// Optional values are undefined
CardValidFrom: cardDetails.StartDate,
IssueNumber: cardDetails.IssueNumber
};
//
// CVV name is different in this request than others, so change it
//
tokeniseDetails.CVV = tokeniseDetails.CardCVV;
delete tokeniseDetails.CardCVV;
//
// Make the request to tokenise
//
const tokeniseP = acquirers.tokeniseCard(config.verificationProvider, tokeniseDetails)
.then((cardDetails) => {
//
// Add the new details on to the card info
//
return _.assign(
{},
account,
cardDetails
);
});
//
// Add the account to the database
//
const addP = tokeniseP.then((accountWithDetails) => {
return Q.nfcall(
mainDB.addObject,
mainDB.collectionAccount,
accountWithDetails,
{},
true
).then((objects) => {
if (objects.length === 0) {
return Q.reject(FAILED_ADD_ACCOUNT);
} else {
return objects[0];
}
}).catch((err) => Q.reject(DB_ERROR_ADD_ACCOUNT));
});
return Q.all([tokeniseP, addP])
.spread((accountWithDetails, addedAccount) => addedAccount);
}
/**
* Adds the initial transaction to the database.
* This uses a new `TransactionStatus` of PENDING_DIRECT_PAYMENT (30) to
* differentiate these transactions from normal transactions or invoices.
*
* @param {Object} client - the client who is paying
* @param {Object} account - the client account to pay from
* @param {Object} merchant - the merchant to be paid
* @param {Object} body - the request body
* @param {string} sessionToken - a session token for the transaction
* @returns {Promise} - a promise for the intialised transaction
*/
function addTransaction(client, account, merchant, body, sessionToken) {
//
// Build the transaction structure
//
const transaction = _.defaults(
{
CustomerAccountID: account._id.toString(),
CustomerClientID: client.ClientID,
CustomerDisplayName: client.DisplayName,
CustomerImage: 'defaultSelfie',
MerchantDeviceToken: 'IntegrationAPI',
MerchantSessionToken: sessionToken,
MerchantAccountID: body.merchantAccount,
MerchantClientID: merchant.ClientID,
MerchantDisplayName: merchant.Merchant[0].CompanyAlias,
MerchantSubDisplayName: merchant.Merchant[0].CompanySubName,
MerchantImage: merchant.Merchant[0].CompanyLogo,
MerchantVATNo: merchant.Merchant[0].VATNo || '',
TransactionStatus: utils.TransactionStatus.PENDING_DIRECT_PAYMENT,
StatusInfo: 'Transaction for direct payment created',
RequestAmount: body.amount,
LastUpdate: new Date()
},
mainDB.blankTransaction()
);
//
// Add the transaction to the database
//
const addP = Q.nfcall(
mainDB.addObject,
mainDB.collectionTransaction,
transaction,
{},
true
);
return addP
.then((objects) => {
if (objects.length === 0) {
return Q.reject(FAILED_ADD_TRANSACTION);
} else {
return objects[0];
}
})
.catch((err) => Q.reject(DB_ERROR_ADD_TRANSACTION));
}
/**
* Attempts to make a payment with the provided information.
*
* @param {Object} client - the client who is paying
* @param {Object} transaction - the transaction to be paid
* @param {Object} body - the request body
* @return {Promise} - Promise for the result of confirming the transaction
*/
function makePayment(client, transaction, body) {
//
// Build the data to send. This includes the unencrypted card details from the request
//
const cardDetails = buildCardDetails(body.cardDetails);
const data = {
TransactionID: transaction._id.toString(),
TipAmount: 0,
initialStatus: utils.TransactionStatus.PENDING_DIRECT_PAYMENT,
cardDetails
};
//
// Need a fake Device as the helper function assume we are coming from a device
//
const fakeDevice = mainDB.blankDevice();
/**
* Call the base implementation
*/
return impl.confirmTransaction(client, fakeDevice, data);
}
/**
* This takes the information provided in the request and turns it into the
* card details format that we would otherwise get from utils/encryptions.js::decryptCard()
*
* @param {Object} cardDetails - card details from the request
* @returns {Object} - card details in the required format
*/
function buildCardDetails(cardDetails) {
const result = {};
//
// Format optional fields
//
if (_.isString(cardDetails.IssueNumber)) {
result.IssueNumber = parseInt(cardDetails.IssueNumber);
}
if (_.isString(cardDetails.StartDate)) {
result.startMonth = cardDetails.StartDate.substr(0, 2);
result.startYear = '20' + cardDetails.ExpiryDate.substr(3, 2);
}
//
// Format required fields.
//
result.expiryMonth = cardDetails.ExpiryDate.substr(0, 2);
result.expiryYear = '20' + cardDetails.ExpiryDate.substr(3, 2);
result.cardNumber = cardDetails.CardPAN;
return result;
}
/**
* Handler for the redeeemPaycode function.
*
* @param {Object} req - the request object
* @param {Object} res - the response object
*/
async function redeemPaycode(req, res) {
const body = req.swagger.params.body.value;
const merchant = req.session.data.Merchant;
const sessionToken = req.session.data.PseudoSession;
//
// Need to build the expected object to match the data in the Apps api:
// @see http://10.0.10.242/w/tricore_architecture/server_interface/payment_commands/redeempaycode/
//
// Note that we don't have a device or session token so we just make them up.
// We also don't have a number of optional fields, so we don't include them
//
const request = {
DeviceToken: 'IntegrationAPI',
SessionToken: sessionToken,
PayCode: body.paycode,
RequestAmount: body.amount,
RequestTip: 0, // No tips through the integration API
AccountID: body.merchantAccount,
//
// Location not available from the Integration API, so set to null
//
Latitude: null,
Longitude: null
};
const responses = [
[
'474',
httpStatus.FORBIDDEN, 474, 'DisplayName is invalid. Please complete customer details'
],
[
'475',
httpStatus.FORBIDDEN, 475, 'CompanyAlias is invalid. Please complete merchant details'
],
[
'476',
httpStatus.BAD_REQUEST, 476, 'Only Merchants can request a tip'
],
[
'175',
httpStatus.BAD_GATEWAY, 175, 'Database offline'
],
[
'176',
httpStatus.BAD_REQUEST, 176, 'Invalid paycode'
],
[
'177',
httpStatus.BAD_GATEWAY, 177, 'Database offline'
],
[
'178',
httpStatus.BAD_GATEWAY, 178, 'Database offline'
],
[
'179',
httpStatus.INTERNAL_SERVER_ERROR, 179, 'Invalid TransactionID'
],
[
'229',
httpStatus.BAD_GATEWAY, 229, 'Database offline'
],
[
'276',
httpStatus.BAD_REQUEST, 276, 'Invalid merchantAccount'
],
[
'491',
httpStatus.FORBIDDEN, 491, 'Invalid billing address for merchantAccount'
],
[
'279',
httpStatus.BAD_GATEWAY, 279, 'Database offline'
],
[
'275',
httpStatus.BAD_REQUEST, 275, 'Deleted merchantAccount'
],
[
'296',
httpStatus.BAD_GATEWAY, 296, 'Database offline'
],
[
'297',
httpStatus.BAD_REQUEST, 297, 'Account cannot receive payments'
],
[
'231',
httpStatus.FORBIDDEN, 231, 'Invalid account image details'
],
[
'180',
httpStatus.BAD_GATEWAY, 180, 'Database offline'
]
];
//
// Call the implementation
//
try {
res = await implRedeem.redeemPaycodeP(merchant, request);
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respond(res, null);
} catch (error) {
if (error) {
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respond(res, error.code);
}
}
}
/**
* Handler for the redeeemPaycode function.
*
* @param {Object} req - the request object
* @param {Object} res - the response object
*/
function getTransactionUpdate(req, res) {
const transactionID = req.swagger.params.TransactionID.value;
const merchant = req.session.data.Merchant;
const sessionToken = req.session.data.PseudoSession;
//
// Need to build the expected object to match the data in the Apps api:
// @see http://10.0.10.242/w/tricore_architecture/server_interface/payment_commands/redeempaycode/
//
// Note that we don't have a device or session token so we just make them up.
// We also don't have a number of optional fields, so we don't include them
//
const request = {
DeviceToken: 'IntegrationAPI',
SessionToken: sessionToken,
TransactionID: transactionID
};
//
// Call the implementation
//
Q.nfcall(implGetUpdate.getTransactionUpdate, request)
.then((result) => {
if (result.code === '10019' || result.code === '10021' || result.code === '10029') {
// Still in progress
res.status(httpStatus.ACCEPTED).json();
} else if (result.code === '10024') {
// Complete succesfully
res.status(httpStatus.OK).json({
CustomerDisplayName: result.CustomerDisplayName,
CustomerSubDisplayName: result.CustomerSubDisplayName || undefined,
TotalAmount: result.TotalAmount
});
} else {
// Other "successes" would be considered errors here (e.g.
// Cancelled, Declined, etc.) So just reject them, and the
// catch will handle them
return Q.reject(result);
}
})
.catch((error) => {
const responses = [
[
'171',
httpStatus.BAD_GATEWAY, 171, 'Database offline'
],
[
'172',
httpStatus.BAD_REQUEST, 172, 'Invalid TransactionID'
],
[
'173',
httpStatus.BAD_REQUEST, 173, 'Invalid TransactionID' // Wrong API key
],
[
'319',
httpStatus.BAD_GATEWAY, 319, 'Database offline'
],
[
'320',
httpStatus.FORBIDDEN, 320, 'Paycode Expired'
],
[
'10022',
httpStatus.GONE, 10022, error.info // Covers various errors
],
[
'10037',
httpStatus.CONFLICT, 10037, 'Transaction refunded'
],
[
'234',
httpStatus.INTERNAL_SERVER_ERROR, 234, 'Invalid TransactionStatus'
]
];
const responseHandler = new responsesUtils.ErrorResponses(responses);
responseHandler.respond(res, error.code);
});
}