Skip to content

Commit

Permalink
Merge pull request #4387 from alphagov/pp-13335-show-3ds-exemption-info
Browse files Browse the repository at this point in the history
PP-13335 PP-13335 Transactions details page - display corporate exemptions data.
  • Loading branch information
iqbalgds authored Feb 14, 2025
2 parents e1e1eb7 + 36dc8e1 commit 091feb2
Show file tree
Hide file tree
Showing 8 changed files with 390 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ module.exports = async function showTransactionDetails (req, res, next) {
const accountId = req.account.gateway_account_id
const chargeId = req.params.chargeId
try {
const data = await ledgerFindWithEvents(accountId, chargeId)
const isCorporateExemptionsEnabled = req.account?.worldpay_3ds_flex?.corporate_exemptions_enabled
const data = await ledgerFindWithEvents(accountId, chargeId, isCorporateExemptionsEnabled)
data.indexFilters = req.session.filters
if (req.session.contextIsAllServiceTransactions) {
data.contextIsAllServiceTransactions = req.session.contextIsAllServiceTransactions
Expand Down
102 changes: 102 additions & 0 deletions src/controllers/transactions/transaction-detail.controller.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
const sinon = require('sinon')
const proxyquire = require('proxyquire')
const ledgerTransactionFixture = require('../../../test/fixtures/ledger-transaction.fixtures')
const { validGatewayAccountResponse } = require('../../../test/fixtures/gateway-account.fixtures')

describe('Transaction details - GET', () => {
let res, next, transaction, transactionServiceSpy

const gatewayAccountId = '15486734'

const EXTERNAL_GATEWAY_ACCOUNT_ID = 'an-external-id'

const transactionId = 'a-transaction-id'

const account = validGatewayAccountResponse(
{
external_id: EXTERNAL_GATEWAY_ACCOUNT_ID,
gateway_account_id: gatewayAccountId,
payment_provider: 'sandbox',
credentials: { username: 'a-username' }
}
)

const req = {
account,
session: {},
params: { chargeId: transactionId }
}

const responseSpy = {
response: sinon.spy()
}

beforeEach(() => {
res = {
render: sinon.spy()
}

next = sinon.spy()

transaction = ledgerTransactionFixture.validTransactionDetailsResponse()
responseSpy.response.resetHistory()
})

describe('passing the isCorporateExemptionsEnabled flag correctly when calling ledgerFindWithEvents', () => {
it('should not set flag when worldpay_3ds_flag is UNDEFINED on the gateway account', async () => {
req.account.worldpay_3ds_flex = undefined

const controller = getControllerWithMocks(transaction, responseSpy)

await controller(req, res, next)

sinon.assert.called(responseSpy.response)
sinon.assert.called(transactionServiceSpy.ledgerFindWithEvents)

sinon.assert.match(transactionServiceSpy.ledgerFindWithEvents.firstCall.args[2], undefined)
})

it('should set flag=false when corporate exemptions = false on the gateway account', async () => {
req.account.worldpay_3ds_flex = {
corporate_exemptions_enabled: false
}

const controller = getControllerWithMocks(transaction, responseSpy)

await controller(req, res, next)

sinon.assert.called(responseSpy.response)
sinon.assert.called(transactionServiceSpy.ledgerFindWithEvents)

sinon.assert.match(transactionServiceSpy.ledgerFindWithEvents.firstCall.args[2], false)
})

it('should set flag=true when corporate exemptions = true on the gateway account', async () => {
req.account.worldpay_3ds_flex = {
corporate_exemptions_enabled: true
}

const controller = getControllerWithMocks(transaction, responseSpy)

await controller(req, res, next)

sinon.assert.called(responseSpy.response)
sinon.assert.called(transactionServiceSpy.ledgerFindWithEvents)

sinon.assert.match(transactionServiceSpy.ledgerFindWithEvents.firstCall.args[2], true)
})
})

function getControllerWithMocks (transaction, responseSpy) {
transactionServiceSpy = {
ledgerFindWithEvents: sinon.spy(() => {
return transaction
})
}

return proxyquire('./transaction-detail.controller', {
'../../services/transaction.service': transactionServiceSpy,
'../../utils/response.js': responseSpy
})
}
})
6 changes: 3 additions & 3 deletions src/services/transaction.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const logCsvFileStreamComplete = function logCsvFileStreamComplete (timestampStr
})
}

const ledgerFindWithEvents = async function ledgerFindWithEvents (accountId, chargeId) {
const ledgerFindWithEvents = async function ledgerFindWithEvents (accountId, chargeId, isCorporateExemptionsEnabled) {
try {
const charge = await Ledger.transaction(chargeId, accountId)
const transactionEvents = await Ledger.events(chargeId, accountId)
Expand All @@ -88,9 +88,9 @@ const ledgerFindWithEvents = async function ledgerFindWithEvents (accountId, cha

if (userIds.length !== 0) {
const users = await userService.findMultipleByExternalIds(userIds)
return transactionView.buildPaymentView(charge, transactionEvents, disputeTransaction, users)
return transactionView.buildPaymentView(charge, transactionEvents, disputeTransaction, isCorporateExemptionsEnabled, users)
} else {
return transactionView.buildPaymentView(charge, transactionEvents, disputeTransaction)
return transactionView.buildPaymentView(charge, transactionEvents, disputeTransaction, isCorporateExemptionsEnabled)
}
} catch (error) {
throw getStatusCodeForError(error)
Expand Down
38 changes: 37 additions & 1 deletion src/utils/transaction-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,38 @@ const DATA_UNAVAILABLE = 'Data unavailable'
const REDACTED_PII_FIELD_VALUE = '<DELETED>'
const LEDGER_TRANSACTION_COUNT_LIMIT = 5000

function append3dSecureData (data) {
if (data?.authorisation_summary?.three_d_security?.required === true) {
return 'Required'
} else if (data?.authorisation_summary?.three_d_security?.required === false) {
return 'Not required'
}
}

function appendCorporateExemptionsData (chargeData, isCorporateExemptionsEnabled) {
const exemptionJson = chargeData?.exemption

const NOT_REQUESTED = 'Not requested'

if (!exemptionJson) {
return
}

if (exemptionJson?.requested === true) {
if (exemptionJson?.type === 'corporate') {
if (chargeData.exemption.outcome?.result) {
return changeCase.sentenceCase(chargeData.exemption.outcome.result)
}
} else if (isCorporateExemptionsEnabled === true) {
return NOT_REQUESTED
}
} else if (exemptionJson?.requested === false) {
if (isCorporateExemptionsEnabled === true) {
return NOT_REQUESTED
}
}
}

module.exports = {
/** prepares the transaction list view */
buildPaymentList: function (connectorData, allCards, gatewayAccountExternalId, filtersResult, filtersDateRangeState, route, backPath, searchPath) {
Expand Down Expand Up @@ -106,7 +138,7 @@ module.exports = {
return connectorData
},

buildPaymentView: function (chargeData, eventsData, disputeTransactionData, users = []) {
buildPaymentView: function (chargeData, eventsData, disputeTransactionData, isCorporateExemptionsEnabled, users = []) {
chargeData.state_friendly = states.getDisplayNameForConnectorState(chargeData.state, chargeData.transaction_type)
chargeData.refund_summary = chargeData.refund_summary || {}

Expand Down Expand Up @@ -173,8 +205,12 @@ module.exports = {
chargeData.dispute = new DisputeTransaction(disputeTransactionData)
}

chargeData.three_d_secure = append3dSecureData(chargeData)
chargeData.corporate_exemption_requested = appendCorporateExemptionsData(chargeData, isCorporateExemptionsEnabled)

delete chargeData.links
delete chargeData.return_url

return chargeData
}
}
Expand Down
182 changes: 182 additions & 0 deletions src/utils/transaction-view.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,186 @@ describe('Transaction view utilities', () => {
expect(paymentView.email).to.equal('[email protected]')
expect(paymentView.card_details.cardholder_name).to.equal('Jane D')
})

describe('3D secure data', () => {
it('should not set `three_d_Secure` field if no 3D secure object', () => {
const transaction = transactionFixtures.validTransactionDetailsResponse({})

const events = transactionFixtures.validTransactionEventsResponse()
const paymentView = buildPaymentView(transaction, events)

expect(paymentView.three_d_secure).equal(undefined)
})

it('should correctly set `three_d_Secure` field to required', () => {
const transaction = transactionFixtures.validTransactionDetailsResponse({
authorisation_summary: {
three_d_security: {
required: true
}
}
})

const events = transactionFixtures.validTransactionEventsResponse()
const paymentView = buildPaymentView(transaction, events)

expect(paymentView.three_d_secure).equal('Required')
})

it('should correctly set `three_d_Secure` field to not required', () => {
const transaction = transactionFixtures.validTransactionDetailsResponse({
authorisation_summary: {
three_d_security: {
required: false
}
}
})

const events = transactionFixtures.validTransactionEventsResponse()
const paymentView = buildPaymentView(transaction, events)

expect(paymentView.three_d_secure).equal('Not required')
})
})

describe('Corporate exemption data', () => {
describe('when requested=true', () => {
describe('when type=corporate', () => {
it('should set to `honoured`', () => {
const transaction = transactionFixtures.validTransactionDetailsResponse({
exemption: {
requested: true,
type: 'corporate',
outcome: {
result: 'honoured'
}
}
})

const events = transactionFixtures.validTransactionEventsResponse()
const paymentView = buildPaymentView(transaction, events)

expect(paymentView.corporate_exemption_requested).to.equal('Honoured')
})

it('should set to `rejected`', () => {
const transaction = transactionFixtures.validTransactionDetailsResponse({
exemption: {
requested: true,
type: 'corporate',
outcome: {
result: 'rejected'
}
}
})

const events = transactionFixtures.validTransactionEventsResponse()
const paymentView = buildPaymentView(transaction, events)

expect(paymentView.corporate_exemption_requested).to.equal('Rejected')
})

it('should set to `out of scope`', async () => {
const transaction = transactionFixtures.validTransactionDetailsResponse({
exemption: {
requested: true,
type: 'corporate',
outcome: {
result: 'out of scope'
}
}
})

const events = transactionFixtures.validTransactionEventsResponse()
const paymentView = buildPaymentView(transaction, events)

expect(paymentView.corporate_exemption_requested).to.equal('Out of scope')
})

it('should not set a value when there is no `outcome` row', async () => {
const transaction = transactionFixtures.validTransactionDetailsResponse({
exemption: {
requested: true,
type: 'corporate'
}
})

const events = transactionFixtures.validTransactionEventsResponse()
const paymentView = buildPaymentView(transaction, events)

expect(paymentView.corporate_exemption_requested).to.equal(undefined)
})
})

describe('when type NOT equal to corporate', () => {
describe('when corporate exemptions are disabled on the account', () => {
it('should not set the field', () => {
const transaction = transactionFixtures.validTransactionDetailsResponse({
exemption: {
requested: true,
outcome: {
result: 'honoured'
}
}
})

const events = transactionFixtures.validTransactionEventsResponse()
const paymentView = buildPaymentView(transaction, events, null, false)

expect(paymentView.corporate_exemption_requested).to.equal(undefined)
})
})

describe('when corporate exemptions are enabled on the account', () => {
it('should set field to `not requested`', () => {
const transaction = transactionFixtures.validTransactionDetailsResponse({
exemption: {
requested: true,
outcome: {
result: 'honoured'
}
}
})

const events = transactionFixtures.validTransactionEventsResponse()
const paymentView = buildPaymentView(transaction, events, null, true)

expect(paymentView.corporate_exemption_requested).to.equal('Not requested')
})
})
})
})

describe('when requested=false', () => {
describe('when corporate exemptions are disabled on the account', () => {
it('should not set the field', () => {
const transaction = transactionFixtures.validTransactionDetailsResponse({
exemption: {
requested: false
}
})

const events = transactionFixtures.validTransactionEventsResponse()
const paymentView = buildPaymentView(transaction, events, null, false)

expect(paymentView.corporate_exemption_requested).to.equal(undefined)
})
})

describe('when corporate exemptions are enabled on the account', () => {
it('should set the field to `not requested`', () => {
const transaction = transactionFixtures.validTransactionDetailsResponse({
exemption: {
requested: false
}
})

const events = transactionFixtures.validTransactionEventsResponse()
const paymentView = buildPaymentView(transaction, events, null, true)

expect(paymentView.corporate_exemption_requested).to.equal('Not requested')
})
})
})
})
})
Loading

0 comments on commit 091feb2

Please sign in to comment.