Skip to content

Commit

Permalink
PP-13261: Merchant initiated tx creds functionality (#4467)
Browse files Browse the repository at this point in the history
* PP-13261: Merchant initiated tx creds functionality
  • Loading branch information
oswaldquek authored Feb 18, 2025
1 parent b278d35 commit ca9e1eb
Show file tree
Hide file tree
Showing 9 changed files with 408 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const gatewayAccount = new GatewayAccount({
credentials: {}
}]
})
const worldpayTasks = new WorldpayTasks(gatewayAccount, SERVICE_ID)
WorldpayTasks.recalculate = () => { return worldpayTasks }

const worldpayDetailsServiceStubs = {
checkCredential: sinon.stub().returns(true),
Expand All @@ -39,8 +41,7 @@ const { req, res, nextRequest, nextStubs, call } = new ControllerTestBuilder('@c
})
.withAccount(gatewayAccount)
.withStubs({
'@utils/response': { response: mockResponse },
'@models/WorldpayTasks.class': { WorldpayTasks: sinon.stub(WorldpayTasks, 'recalculate').returns(gatewayAccount, SERVICE_ID) }
'@utils/response': { response: mockResponse }
})
.build()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const { response } = require('@utils/response')
const formatSimplifiedAccountPathsFor = require('@utils/simplified-account/format/format-simplified-account-paths-for')
const paths = require('@root/paths')
const { body, validationResult } = require('express-validator')
const formatValidationErrors = require('@utils/simplified-account/format/format-validation-errors')
const WorldpayCredential = require('@models/gateway-account-credential/WorldpayCredential.class')
const worldpayDetailsService = require('@services/worldpay-details.service')
const { WorldpayTasks } = require('@models/WorldpayTasks.class')

function get (req, res) {
const existingCredentials = req.account.getCurrentCredential().credentials?.recurringMerchantInitiated || {}

return response(req, res, 'simplified-account/settings/worldpay-details/recurring-merchant-initiated-credentials', {
backLink: formatSimplifiedAccountPathsFor(paths.simplifiedAccount.settings.worldpayDetails.index,
req.service.externalId, req.account.type),
credentials: existingCredentials
})
}

const credentialsValidations = [
body('merchantCode').not().isEmpty().withMessage('Enter your merchant code'),
body('username').not().isEmpty().withMessage('Enter your username'),
body('password').not().isEmpty().withMessage('Enter your password')
]

async function post (req, res) {
await Promise.all(credentialsValidations.map(validation => validation.run(req)))
const validationErrors = validationResult(req)
if (!validationErrors.isEmpty()) {
const formattedErrors = formatValidationErrors(validationErrors)
return errorResponse(req, res, {
summary: formattedErrors.errorSummary,
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: '#merchant-code'
}
]
})
}

await worldpayDetailsService.updateRecurringMerchantInitiatedCredentials(
req.service.externalId,
req.account.type,
req.account.getCurrentCredential().externalId,
req.user.externalId,
credential
)

// if this is the last task to be completed
// show a success banner
const previousTasks = new WorldpayTasks(req.account, req.service.externalId)
if (previousTasks.incompleteTasks) {
const recalculatedTasks = await WorldpayTasks.recalculate(req.service.externalId, req.account.type)
if (!recalculatedTasks.incompleteTasks) {
req.flash('messages', {
state: 'success',
icon: '✓',
heading: 'Service connected to Worldpay',
body: 'This service can now take payments'
})
}
}

return res.redirect(formatSimplifiedAccountPathsFor(paths.simplifiedAccount.settings.worldpayDetails.index,
req.service.externalId, req.account.type))
}

const errorResponse = (req, res, errors) => {
return response(req, res, 'simplified-account/settings/worldpay-details/recurring-merchant-initiated-credentials', {
errors,
credentials: {
merchantCode: req.body.merchantCode,
username: req.body.username,
password: req.body.password
},
backLink: formatSimplifiedAccountPathsFor(paths.simplifiedAccount.settings.worldpayDetails.index,
req.service.externalId, req.account.type)
})
}

module.exports = {
get, post
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
const sinon = require('sinon')
const ControllerTestBuilder = require('@test/test-helpers/simplified-account/controllers/ControllerTestBuilder.class')
const Service = require('@models/Service.class')
const GatewayAccount = require('@models/GatewayAccount.class')
const { expect } = require('chai')
const formatSimplifiedAccountPathsFor = require('@utils/simplified-account/format/format-simplified-account-paths-for')
const paths = require('@root/paths')
const WorldpayCredential = require('@models/gateway-account-credential/WorldpayCredential.class')
const { WorldpayTasks } = require('@models/WorldpayTasks.class')
const mockResponse = sinon.spy()

const ACCOUNT_TYPE = 'live'
const SERVICE_ID = 'service-id-123abc'
const gatewayAccount = new GatewayAccount({
type: ACCOUNT_TYPE,
allow_moto: true,
gateway_account_id: 1,
gateway_account_credentials: [{
external_id: 'creds-id',
payment_provider: 'worldpay',
state: 'CREATED',
created_date: '2024-11-29T11:58:36.214Z',
gateway_account_id: 1,
credentials: {}
}]
})
const worldpayTasks = new WorldpayTasks(gatewayAccount, SERVICE_ID)
WorldpayTasks.recalculate = () => { return worldpayTasks }

const worldpayDetailsServiceStubs = {
checkCredential: sinon.stub().returns(true),
updateRecurringMerchantInitiatedCredentials: sinon.spy()
}

const { req, res, nextRequest, nextStubs, call } = new ControllerTestBuilder('@controllers/simplified-account/settings/worldpay-details/recurring-merchant-initiated-credentials/recurring-merchant-initiated-credentials.controller')
.withService(new Service({
external_id: SERVICE_ID
}))
.withUser({
externalId: 'a-user-external-id'
})
.withAccount(gatewayAccount)
.withStubs({
'@utils/response': { response: mockResponse }
})
.build()

describe('Controller: settings/worldpay-details/recurring-merchant-initiated-credentials', () => {
describe('get', () => {
describe('when credentials do not exist', () => {
before(() => {
call('get')
})

it('should call the response method', () => {
expect(mockResponse.called).to.be.true // eslint-disable-line
})

it('should pass req, res and template path to the response method', () => {
mockResponse.should.have.been.calledWith(req, res, 'simplified-account/settings/worldpay-details/recurring-merchant-initiated-credentials')
})

it('should pass context data with no credentials to the response method', () => {
mockResponse.should.have.been.calledWith(sinon.match.any, sinon.match.any, sinon.match.any, {
credentials: {},
backLink: formatSimplifiedAccountPathsFor(paths.simplifiedAccount.settings.worldpayDetails.index, SERVICE_ID, ACCOUNT_TYPE)
})
})
})
describe('when credentials exist', () => {
before(() => {
nextRequest({
account: {
gatewayAccountCredentials: [{
credentials: {
recurringMerchantInitiated: {
merchantCode: 'a-merchant-code',
username: 'a-username'
}
}
}]
}
})
call('get')
})
it('should call the response method', () => {
expect(mockResponse.called).to.be.true // eslint-disable-line
})

it('should pass req, res and template path to the response method', () => {
mockResponse.should.have.been.calledWith(req, res, 'simplified-account/settings/worldpay-details/recurring-merchant-initiated-credentials')
})

it('should pass context data with no credentials to the response method', () => {
mockResponse.should.have.been.calledWith(sinon.match.any, sinon.match.any, sinon.match.any, {
credentials: {
merchantCode: 'a-merchant-code',
username: 'a-username'
},
backLink: formatSimplifiedAccountPathsFor(paths.simplifiedAccount.settings.worldpayDetails.index, SERVICE_ID, ACCOUNT_TYPE)
})
})
})
})

describe('post', () => {
beforeEach(() => {
nextRequest({
body: {
merchantCode: 'a-merchant-code',
username: 'a-username',
password: 'a-password' // pragma: allowlist secret
}
})
})

describe('when the worldpay credential check fails', () => {
beforeEach(async () => {
nextStubs({
'@services/worldpay-details.service': {
checkCredential: sinon.stub().returns(false),
updateRecurringMerchantInitiatedCredentials: sinon.spy()
}
})
await call('post')
})
it('should render the form with an error', () => {
mockResponse.should.have.been.calledOnce // eslint-disable-line no-unused-expressions
mockResponse.should.have.been.calledWith(
sinon.match.any,
sinon.match.any,
'simplified-account/settings/worldpay-details/recurring-merchant-initiated-credentials',
{
errors: {
summary: [
{ text: 'Check your Worldpay credentials, failed to link your account to Worldpay with credentials provided', href: '#merchant-code' }
]
},
credentials: {
merchantCode: 'a-merchant-code',
username: 'a-username',
password: 'a-password' // pragma: allowlist secret
},
backLink: formatSimplifiedAccountPathsFor(paths.simplifiedAccount.settings.worldpayDetails.index, SERVICE_ID, ACCOUNT_TYPE)
})
})
})

describe('when the worldpay credential check passes', () => {
beforeEach(async () => {
nextStubs({
'@services/worldpay-details.service': worldpayDetailsServiceStubs
})
await call('post')
})
it('should call the worldpay details service to update the recurring customer initiated credentials', () => {
worldpayDetailsServiceStubs.updateRecurringMerchantInitiatedCredentials.should.have.been.calledOnce // eslint-disable-line no-unused-expressions
const credential = new WorldpayCredential()
.withMerchantCode('a-merchant-code')
.withUsername('a-username')
.withPassword('a-password') // pragma: allowlist secret
worldpayDetailsServiceStubs.updateRecurringMerchantInitiatedCredentials.should.have.been.calledWith(SERVICE_ID, ACCOUNT_TYPE, 'creds-id', 'a-user-external-id', credential)
})
it('should call the redirect method with the worldpay details index path on success', () => {
res.redirect.should.have.been.calledWith(formatSimplifiedAccountPathsFor(paths.simplifiedAccount.settings.worldpayDetails.index, SERVICE_ID, ACCOUNT_TYPE))
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ module.exports = {
get,
oneOffCustomerInitiatedCredentials: require('./credentials/worldpay-credentials.controller'),
flexCredentials: require('./flex-credentials/worldpay-flex-credentials.controller'),
recurringCustomerInitiatedCredentials: require('./recurring-customer-initiated-credentials/recurring-customer-initiated-credentials.controller')
recurringCustomerInitiatedCredentials: require('./recurring-customer-initiated-credentials/recurring-customer-initiated-credentials.controller'),
recurringMerchantInitiatedCredentials: require('./recurring-merchant-initiated-credentials/recurring-merchant-initiated-credentials.controller')
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ const safeOperation = (op, request) => {
recurringCustomerInitiated: (value) => {
request.updates.push({ op, path: 'credentials/worldpay/recurring_customer_initiated', value })
return request
},
recurringMerchantInitiated: (value) => {
request.updates.push({ op, path: 'credentials/worldpay/recurring_merchant_initiated', value })
return request
}
}
},
Expand Down
16 changes: 16 additions & 0 deletions src/services/worldpay-details.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,21 @@ async function updateRecurringCustomerInitiatedCredentials (serviceExternalId, a
return connectorClient.patchGatewayAccountCredentialsByServiceExternalIdAndAccountType(serviceExternalId, accountType, credentialId, patchRequest)
}

/**
*
* @param {String} serviceExternalId
* @param {String} accountType
* @param {String} credentialId
* @param {String} userExternalId
* @param {WorldpayCredential} credential
* @returns {Promise<GatewayAccountCredential>}
*/
async function updateRecurringMerchantInitiatedCredentials (serviceExternalId, accountType, credentialId, userExternalId, credential) {
const patchRequest = new GatewayAccountCredentialUpdateRequest(userExternalId)
.replace().credentials().recurringMerchantInitiated(credential.toJson())
return connectorClient.patchGatewayAccountCredentialsByServiceExternalIdAndAccountType(serviceExternalId, accountType, credentialId, patchRequest)
}

/**
*
* @param {String} serviceExternalId
Expand Down Expand Up @@ -123,5 +138,6 @@ module.exports = {
check3dsFlexCredential,
update3dsFlexCredentials,
updateIntegrationVersion3ds,
updateRecurringMerchantInitiatedCredentials,
updateRecurringCustomerInitiatedCredentials
}
2 changes: 2 additions & 0 deletions src/simplified-account-routes.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ca9e1eb

Please sign in to comment.