785 lines
26 KiB
JavaScript
785 lines
26 KiB
JavaScript
|
/* 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);
|
||
|
});
|
||
|
}
|