/* 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); }); }