From bb0f7fb75d749d0d2958ddc7b9508a28fa57d322 Mon Sep 17 00:00:00 2001 From: Dom Belcher Date: Tue, 17 Dec 2024 15:31:50 +0000 Subject: [PATCH] PP-13313 Validate Credentials with Worldpay (#4386) * PP-13313 Validate Credentials with Worldpay --- .../worldpay-credentials.controller.js | 22 ++++++++ app/models/GatewayAccount.class.js | 2 +- app/models/WorldpayTasks.class.js | 3 +- .../Credential.class.js | 52 +++++++++++++++++++ .../GatewayAccountCredential.class.js | 14 ++--- ...wayAccountCredentialUpdateRequest.class.js | 32 ++++++++++++ .../ValidationResult.class.js | 24 +++++++++ .../WorldpayCredential.class.js | 42 +++++++++++++++ app/services/clients/connector.client.js | 36 +++++++++++++ app/services/worldpay-details.service.js | 30 +++++++++++ 10 files changed, 245 insertions(+), 12 deletions(-) create mode 100644 app/models/gateway-account-credential/Credential.class.js rename app/models/{ => gateway-account-credential}/GatewayAccountCredential.class.js (63%) create mode 100644 app/models/gateway-account-credential/GatewayAccountCredentialUpdateRequest.class.js create mode 100644 app/models/gateway-account-credential/ValidationResult.class.js create mode 100644 app/models/gateway-account-credential/WorldpayCredential.class.js create mode 100644 app/services/worldpay-details.service.js diff --git a/app/controllers/simplified-account/settings/worldpay-details/credentials/worldpay-credentials.controller.js b/app/controllers/simplified-account/settings/worldpay-details/credentials/worldpay-credentials.controller.js index e242a262a..b0b425ceb 100644 --- a/app/controllers/simplified-account/settings/worldpay-details/credentials/worldpay-credentials.controller.js +++ b/app/controllers/simplified-account/settings/worldpay-details/credentials/worldpay-credentials.controller.js @@ -3,6 +3,8 @@ const formatSimplifiedAccountPathsFor = require('@utils/simplified-account/forma const paths = require('@root/paths') const { body, validationResult } = require('express-validator') const formatValidationErrors = require('@utils/simplified-account/format/format-validation-errors') +const worldpayDetailsService = require('@services/worldpay-details.service') +const WorldpayCredential = require('@models/gateway-account-credential/WorldpayCredential.class') function get (req, res) { return response(req, res, 'simplified-account/settings/worldpay-details/credentials', { @@ -33,6 +35,26 @@ async function post (req, res) { formErrors: formattedErrors.formErrors }) } + + const credential = new WorldpayCredential() + .withMerchantCode(req.body.merchantCode) + .withUsername(req.body.username) + .withPassword(req.body.password) + + const isValid = await worldpayDetailsService.checkCredential(req.service.externalId, req.account.type, credential) + if (!isValid) { + return errorResponse(req, res, { + summary: [ + { + text: 'Check your Worldpay credentials, failed to link your account to Worldpay with credentials provided', + href: '#merchantCode' + } + ] + }) + } + + return res.redirect(formatSimplifiedAccountPathsFor(paths.simplifiedAccount.settings.worldpayDetails.index, + req.service.externalId, req.account.type)) } const errorResponse = (req, res, errors) => { diff --git a/app/models/GatewayAccount.class.js b/app/models/GatewayAccount.class.js index 15f3991ae..79229f1d6 100644 --- a/app/models/GatewayAccount.class.js +++ b/app/models/GatewayAccount.class.js @@ -1,4 +1,4 @@ -const { GatewayAccountCredential, CREDENTIAL_STATE } = require('@models/GatewayAccountCredential.class') +const { GatewayAccountCredential, CREDENTIAL_STATE } = require('@models/gateway-account-credential/GatewayAccountCredential.class') /** * @class GatewayAccount diff --git a/app/models/WorldpayTasks.class.js b/app/models/WorldpayTasks.class.js index 02dd82b46..48507e8e1 100644 --- a/app/models/WorldpayTasks.class.js +++ b/app/models/WorldpayTasks.class.js @@ -6,6 +6,7 @@ const paths = require('@root/paths') class WorldpayTasks { /** * @param {GatewayAccount} gatewayAccount + * @param {Service} service */ constructor (gatewayAccount, service) { this.tasks = [] @@ -21,7 +22,7 @@ class WorldpayTasks { linkText: 'Link your Worldpay account with GOV.UK Pay', complete: true } - if (credential === null || credential.credentials.one_off_customer_initiated === null) { + if (!credential || !credential.credentials.oneOffCustomerInitiated) { worldpayCredentials.complete = false } this.tasks.push(worldpayCredentials) diff --git a/app/models/gateway-account-credential/Credential.class.js b/app/models/gateway-account-credential/Credential.class.js new file mode 100644 index 000000000..73114ec0a --- /dev/null +++ b/app/models/gateway-account-credential/Credential.class.js @@ -0,0 +1,52 @@ +const WorldpayCredential = require('./WorldpayCredential.class') + +class Credential { + /** + * + * @param {String} stripeAccountId + * @returns {Credential} + */ + withStripeAccountId (stripeAccountId) { + if (stripeAccountId) { + this.stripeAccountId = stripeAccountId + } + return this + } + + /** + * + * @param {WorldpayCredential} oneOffCustomerInitiated + * @returns {Credential} + */ + withOneOffCustomerInitiated (oneOffCustomerInitiated) { + if (oneOffCustomerInitiated) { + this.oneOffCustomerInitiated = oneOffCustomerInitiated + } + return this + } + + /** @deprecated this is a temporary compatability fix! If you find yourself using this for new code + * you should instead add any rawResponse data as part of the constructor */ + withRawResponse (data) { + /** @deprecated this is a temporary compatability fix! If you find yourself using this for new code + * you should instead add any rawResponse data as part of the constructor */ + this.rawResponse = data + return this + } + + toJson () { + return { + ...this.stripeAccountId && { stripe_account_id: this.stripeAccountId }, + ...this.oneOffCustomerInitiated && { one_off_customer_initiated: this.oneOffCustomerInitiated.toJson() } + } + } + + static fromJson (data) { + return new Credential() + .withStripeAccountId(data?.stripe_account_id) + .withOneOffCustomerInitiated(WorldpayCredential.fromJson(data?.one_off_customer_initiated)) + .withRawResponse(data) + } +} + +module.exports = Credential diff --git a/app/models/GatewayAccountCredential.class.js b/app/models/gateway-account-credential/GatewayAccountCredential.class.js similarity index 63% rename from app/models/GatewayAccountCredential.class.js rename to app/models/gateway-account-credential/GatewayAccountCredential.class.js index 667039124..f01007234 100644 --- a/app/models/GatewayAccountCredential.class.js +++ b/app/models/gateway-account-credential/GatewayAccountCredential.class.js @@ -1,3 +1,5 @@ +const Credential = require('./Credential.class') + const CREDENTIAL_STATE = { CREATED: 'CREATED', ENTERED: 'ENTERED', @@ -10,7 +12,7 @@ class GatewayAccountCredential { constructor (data) { this.externalId = data.external_id this.paymentProvider = data.payment_provider - this.credentials = new Credential(data.credentials) + this.credentials = Credential.fromJson(data.credentials) this.state = data.state this.createdDate = data.created_date this.activeStartDate = data.active_start_date @@ -19,14 +21,6 @@ class GatewayAccountCredential { } } -class Credential { - constructor (data) { - this.stripeAccountId = data.stripe_account_id - /** @deprecated this is a temporary compatability fix! If you find yourself using this for new code - * you should instead add any rawResponse data as part of the constructor */ - this.rawResponse = data - } -} - module.exports.GatewayAccountCredential = GatewayAccountCredential +module.exports.Credential = Credential module.exports.CREDENTIAL_STATE = CREDENTIAL_STATE diff --git a/app/models/gateway-account-credential/GatewayAccountCredentialUpdateRequest.class.js b/app/models/gateway-account-credential/GatewayAccountCredentialUpdateRequest.class.js new file mode 100644 index 000000000..8f068372f --- /dev/null +++ b/app/models/gateway-account-credential/GatewayAccountCredentialUpdateRequest.class.js @@ -0,0 +1,32 @@ +'use strict' + +class GatewayAccountCredentialUpdateRequest { + /** + * @param {String} userExternalId + */ + constructor (userExternalId) { + this.updates = [{ + op: 'replace', + path: 'last_updated_by_user_external_id', + value: userExternalId + }] + } + + replace () { + return safeOperation('replace', this) + } + + formatPayload () { + return this.updates + } +} + +const safeOperation = (op, request) => { + return { + credentials: (value) => { + request.updates.push({ op, path: 'credentials', value }) + return request + } + } +} +module.exports = { GatewayAccountCredentialUpdateRequest } diff --git a/app/models/gateway-account-credential/ValidationResult.class.js b/app/models/gateway-account-credential/ValidationResult.class.js new file mode 100644 index 000000000..5351cfb96 --- /dev/null +++ b/app/models/gateway-account-credential/ValidationResult.class.js @@ -0,0 +1,24 @@ +class ValidationResult { + /** + * + * @param {String} result + */ + withResult (result) { + if (result) { + this.result = result + } + return this + } + + /** + * + * @param data + * @returns {ValidationResult} + */ + static fromJson (data) { + return new ValidationResult() + .withResult(data?.result) + } +} + +module.exports = ValidationResult diff --git a/app/models/gateway-account-credential/WorldpayCredential.class.js b/app/models/gateway-account-credential/WorldpayCredential.class.js new file mode 100644 index 000000000..a1b3a55b6 --- /dev/null +++ b/app/models/gateway-account-credential/WorldpayCredential.class.js @@ -0,0 +1,42 @@ +class WorldpayCredential { + withMerchantCode (merchantCode) { + if (merchantCode) { + this.merchantCode = merchantCode + } + return this + } + + withUsername (username) { + if (username) { + this.username = username + } + return this + } + + withPassword (password) { + if (password) { + this.password = password + } + return this + } + + toJson () { + return { + ...this.merchantCode && { merchant_code: this.merchantCode }, + ...this.username && { username: this.username }, + ...this.password && { password: this.password } + } + } + + static fromJson (data) { + if (!data) { + return undefined + } + return new WorldpayCredential() + .withMerchantCode(data?.merchant_code) + .withUsername(data?.username) + .withPassword(data?.password) + } +} + +module.exports = WorldpayCredential diff --git a/app/services/clients/connector.client.js b/app/services/clients/connector.client.js index 350965216..873b2c6c6 100644 --- a/app/services/clients/connector.client.js +++ b/app/services/clients/connector.client.js @@ -6,6 +6,7 @@ const { configureClient } = require('./base/config') const StripeAccountSetup = require('../../models/StripeAccountSetup.class') const StripeAccount = require('../../models/StripeAccount.class') const GatewayAccount = require('@models/GatewayAccount.class') +const ValidationResult = require('@models/gateway-account-credential/ValidationResult.class') // Constants const SERVICE_NAME = 'connector' @@ -132,6 +133,25 @@ ConnectorClient.prototype = { return response.data }, + /** + * + * @param {String} serviceExternalId + * @param {String} accountType + * @param {String} credentialsId + * @param {Object} payload + * @returns {Promise} + */ + patchGatewayAccountCredentialsByServiceIdAndAccountType: async function (serviceExternalId, accountType, credentialsId, payload) { + const url = `${this.connectorUrl}/v1/api/service/{serviceExternalId}/account/{accountType}/credentials/{credentialsId}` + .replace('{serviceExternalId}', encodeURIComponent(serviceExternalId)) + .replace('{accountType}', encodeURIComponent(accountType)) + .replace('{credentialsId}', encodeURIComponent(credentialsId)) + + configureClient(client, url) + const response = await client.patch(url, payload, 'patch gateway account credentials') + return response.data + }, + patchGooglePayGatewayMerchantId: async function (gatewayAccountId, gatewayAccountCredentialsId, googlePayGatewayMerchantId, userExternalId) { const url = `${this.connectorUrl}/v1/api/accounts/{accountId}/credentials/{credentialsId}` .replace('{accountId}', encodeURIComponent(gatewayAccountId)) @@ -217,6 +237,22 @@ ConnectorClient.prototype = { return response.data }, + /** + * + * @param {String} serviceExternalId + * @param {String} accountType + * @param {WorldpayCredential} credentials + * @returns {Promise} + */ + postCheckWorldpayCredentialByServiceExternalIdAndAccountType: async function (serviceExternalId, accountType, credentials) { + const url = `${this.connectorUrl}/v1/api/service/{serviceExternalId}/account/{accountType}/worldpay/check-credentials` + .replace('{serviceExternalId}', encodeURIComponent(serviceExternalId)) + .replace('{accountType}', encodeURIComponent(accountType)) + configureClient(client, url) + const response = await client.post(url, credentials.toJson(), 'Check Worldpay credentials') + return ValidationResult.fromJson(response.data) + }, + /** * * @param {Object} params diff --git a/app/services/worldpay-details.service.js b/app/services/worldpay-details.service.js new file mode 100644 index 000000000..67a84f2e4 --- /dev/null +++ b/app/services/worldpay-details.service.js @@ -0,0 +1,30 @@ +const { ConnectorClient } = require('./clients/connector.client') +const logger = require('../utils/logger')(__filename) + +const connectorClient = new ConnectorClient(process.env.CONNECTOR_URL) + +/** + * + * @param {String} serviceExternalId + * @param {String} accountType + * @param {WorldpayCredential} credential + * @returns {Promise} + */ +async function checkCredential (serviceExternalId, accountType, credential) { + const credentialCheck = await connectorClient.postCheckWorldpayCredentialByServiceExternalIdAndAccountType( + serviceExternalId, + accountType, + credential + ) + if (credentialCheck.result !== 'valid') { + logger.warn(`Credentials provided for service external ID [${serviceExternalId}], account type [${accountType}] failed validation with Worldpay`) + return false + } + + logger.info(`Successfully validated credentials for service external ID [${serviceExternalId}], account type [${accountType}] with Worldpay`) + return true +} + +module.exports = { + checkCredential +}