From 3b978eb6a632b49f0adb2bff261c4f51a20a8f88 Mon Sep 17 00:00:00 2001 From: James Peacock Date: Wed, 4 Dec 2024 17:12:21 +0000 Subject: [PATCH] PP-13312 Get controller, templates and unit tests --- .../sass/components/service-settings.scss | 12 +- .../card-types/card-types.controller.js | 17 ++- .../card-types/card-types.controller.test.js | 131 ++++++++++++++++++ app/services/card-types.service.js | 17 +++ app/services/clients/connector.client.js | 14 ++ .../format/format-card-types.js | 72 ++++++++++ .../format/format-card-types.test.js | 78 +++++++++++ .../card-types/cardTypesCheckboxes.njk | 44 ++++++ .../settings/card-types/cardTypesList.njk | 21 +++ .../settings/card-types/index.njk | 18 ++- .../ControllerTestBuilder.class.js | 6 + 11 files changed, 425 insertions(+), 5 deletions(-) create mode 100644 app/controllers/simplified-account/settings/card-types/card-types.controller.test.js create mode 100644 app/services/card-types.service.js create mode 100644 app/utils/simplified-account/format/format-card-types.js create mode 100644 app/utils/simplified-account/format/format-card-types.test.js create mode 100644 app/views/simplified-account/settings/card-types/cardTypesCheckboxes.njk create mode 100644 app/views/simplified-account/settings/card-types/cardTypesList.njk diff --git a/app/assets/sass/components/service-settings.scss b/app/assets/sass/components/service-settings.scss index c9e510140..69f5709ed 100644 --- a/app/assets/sass/components/service-settings.scss +++ b/app/assets/sass/components/service-settings.scss @@ -1,4 +1,4 @@ -// https://getbem.com/naming/ +s// https://getbem.com/naming/ .service-settings-nav { @include govuk-font(16); @@ -43,15 +43,23 @@ width: 30% !important; } } - .task-list { a:visited, a:link { color: govuk-colour("blue"); } + a:hover { color: govuk-colour("dark-blue"); } + a:active, a:focus { color: govuk-colour("black"); } } + +.card-types-list { + display: inline-block; + margin-bottom: govuk-spacing(2); + padding: 8px govuk-spacing(1) +} + diff --git a/app/controllers/simplified-account/settings/card-types/card-types.controller.js b/app/controllers/simplified-account/settings/card-types/card-types.controller.js index 199f1a357..ea8a5dc9e 100644 --- a/app/controllers/simplified-account/settings/card-types/card-types.controller.js +++ b/app/controllers/simplified-account/settings/card-types/card-types.controller.js @@ -1,7 +1,20 @@ const { response } = require('@utils/response') +const { formatCardTypesForTemplate } = require('@utils/simplified-account/format/format-card-types') +const { getAllCardTypes, getAcceptedCardTypesForServiceAndAccountType } = require('@services/card-types.service') -function get (req, res) { - response(req, res, 'simplified-account/settings/card-types/index', { }) +async function get (req, res, next) { + const serviceId = req.service.externalId + const accountType = req.account.type + const isAdminUser = req.user.isAdminUserForService(serviceId) + try { + const { card_types: allCards } = await getAllCardTypes() + const { card_types: acceptedCards } = await getAcceptedCardTypesForServiceAndAccountType(serviceId, accountType) + const cardTypes = formatCardTypesForTemplate(allCards, acceptedCards, req.account, isAdminUser) + response(req, res, 'simplified-account/settings/card-types/index', + { cardTypes, isAdminUser }) + } catch (err) { + next(err) + } } module.exports = { diff --git a/app/controllers/simplified-account/settings/card-types/card-types.controller.test.js b/app/controllers/simplified-account/settings/card-types/card-types.controller.test.js new file mode 100644 index 000000000..1656ed6d6 --- /dev/null +++ b/app/controllers/simplified-account/settings/card-types/card-types.controller.test.js @@ -0,0 +1,131 @@ +const sinon = require('sinon') +const { expect } = require('chai') +const User = require('@models/User.class') +const userFixtures = require('@test/fixtures/user.fixtures') +const proxyquire = require('proxyquire') + +const ACCOUNT_TYPE = 'live' +const SERVICE_ID = 'service-id-123abc' + +const adminUser = new User(userFixtures.validUserResponse({ + external_id: 'user-id-for-admin-user', + service_roles: { + service: { + service: { external_id: SERVICE_ID }, + role: { name: 'admin' } + } + } +})) +const viewOnlyUser = new User(userFixtures.validUserResponse( + { + external_id: 'user-id-for-view-only-user', + service_roles: { + service: + { + service: { external_id: SERVICE_ID }, + role: { name: 'view-only' } + } + } + })) + +const allCardTypes = [{ + id: 'id-001', + brand: 'visa', + label: 'Visa', + type: 'DEBIT', + requires3ds: false +}, +{ + id: 'id-002', + brand: 'visa', + label: 'Visa', + type: 'CREDIT', + requires3ds: false +}] + +let req, res, responseStub, getAllCardTypesStub, getAcceptedCardTypesForServiceAndAccountTypeStub, cardTypesController + +const getController = (stubs = {}) => { + return proxyquire('./card-types.controller', { + '@utils/response': { response: stubs.response }, + '@services/card-types.service': { + getAllCardTypes: stubs.getAllCardTypes, + getAcceptedCardTypesForServiceAndAccountType: stubs.getAcceptedCardTypesForServiceAndAccountType + } + }) +} + +const setupTest = (method, user, additionalReqProps = {}, additionalStubs = {}) => { + responseStub = sinon.spy() + getAllCardTypesStub = sinon.stub().returns({ card_types: allCardTypes }) + getAcceptedCardTypesForServiceAndAccountTypeStub = sinon.stub().resolves({ card_types: [allCardTypes[0]] }) + + cardTypesController = getController({ + response: responseStub, + getAllCardTypes: getAllCardTypesStub, + getAcceptedCardTypesForServiceAndAccountType: getAcceptedCardTypesForServiceAndAccountTypeStub, + ...additionalStubs + }) + res = { + redirect: sinon.spy() + } + req = { + user: user, + service: { + externalId: SERVICE_ID + }, + account: { + type: ACCOUNT_TYPE + }, + ...additionalReqProps + } + cardTypesController[method](req, res) +} + +describe('Controller: settings/card-types', () => { + describe('get for admin user', () => { + before(() => setupTest('get', adminUser)) + + it('should call the response method', () => { + expect(responseStub.called).to.be.true // eslint-disable-line + }) + + it('should pass req, res and template path to the response method', () => { + expect(responseStub.args[0][0]).to.deep.equal(req) + expect(responseStub.args[0][1]).to.deep.equal(res) + expect(responseStub.args[0][2]).to.equal('simplified-account/settings/card-types/index') + }) + + it('should pass context data to the response method', () => { + expect(responseStub.args[0][3]).to.have.property('cardTypes').to.have.property('debitCards').length(1) + expect(responseStub.args[0][3].cardTypes.debitCards[0]).to.have.property('text').to.equal('Visa debit') + expect(responseStub.args[0][3].cardTypes.debitCards[0]).to.have.property('checked').to.equal(true) + expect(responseStub.args[0][3]).to.have.property('cardTypes').to.have.property('creditCards').length(1) + expect(responseStub.args[0][3].cardTypes.creditCards[0]).to.have.property('text').to.equal('Visa credit') + expect(responseStub.args[0][3].cardTypes.creditCards[0]).to.have.property('checked').to.equal(false) + expect(responseStub.args[0][3]).to.have.property('isAdminUser').to.equal(true) + }) + }) + + describe('get for non-admin user', () => { + before(() => setupTest('get', viewOnlyUser)) + + it('should call the response method', () => { + expect(responseStub.called).to.be.true // eslint-disable-line + }) + + it('should pass req, res and template path to the response method', () => { + expect(responseStub.args[0][0]).to.deep.equal(req) + expect(responseStub.args[0][1]).to.deep.equal(res) + expect(responseStub.args[0][2]).to.equal('simplified-account/settings/card-types/index') + }) + + it('should pass context data to the response method', () => { + expect(responseStub.args[0][3]).to.have.property('cardTypes').to.have.property('Enabled debit cards').to.have.length(1) + expect(responseStub.args[0][3].cardTypes).to.have.property('Not enabled debit cards').to.have.length(0) + expect(responseStub.args[0][3].cardTypes).to.have.property('Enabled credit cards').to.have.length(0) + expect(responseStub.args[0][3].cardTypes).to.have.property('Not enabled credit cards').to.have.length(1) + expect(responseStub.args[0][3]).to.have.property('isAdminUser').to.equal(false) + }) + }) +}) diff --git a/app/services/card-types.service.js b/app/services/card-types.service.js new file mode 100644 index 000000000..dc0de9d7a --- /dev/null +++ b/app/services/card-types.service.js @@ -0,0 +1,17 @@ +'use strict' + +const ConnectorClient = require('./clients/connector.client.js').ConnectorClient +const connectorClient = new ConnectorClient(process.env.CONNECTOR_URL) + +async function getAllCardTypes () { + return connectorClient.getAllCardTypes() +} + +async function getAcceptedCardTypesForServiceAndAccountType (serviceId, accountType) { + return connectorClient.getAcceptedCardsForServiceAndAccountType(serviceId, accountType) +} + +module.exports = { + getAllCardTypes, + getAcceptedCardTypesForServiceAndAccountType +} diff --git a/app/services/clients/connector.client.js b/app/services/clients/connector.client.js index a559dc64d..a83570c88 100644 --- a/app/services/clients/connector.client.js +++ b/app/services/clients/connector.client.js @@ -257,6 +257,20 @@ ConnectorClient.prototype = { return response.data }, + /** + * Retrieves the accepted card Types for the given account + * @param gatewayAccountId (required) + * @returns {Promise} + */ + getAcceptedCardsForServiceAndAccountType: async function (serviceId, accountType) { + const url = `${this.connectorUrl}/v1/frontend/service/{serviceId}/account/{accountType}/card-types` + .replace('{serviceId}', encodeURIComponent(serviceId)) + .replace('{accountType}', encodeURIComponent(accountType)) + configureClient(client, url) + const response = await client.get(url, 'get accepted card types for account') + return response.data + }, + /** * Updates the accepted card Types for to the given gateway account * @param gatewayAccountId (required) diff --git a/app/utils/simplified-account/format/format-card-types.js b/app/utils/simplified-account/format/format-card-types.js new file mode 100644 index 000000000..70e000f63 --- /dev/null +++ b/app/utils/simplified-account/format/format-card-types.js @@ -0,0 +1,72 @@ +const formatLabel = (card) => { + if (card.brand === 'visa' || card.brand === 'master-card') { + return `${card.label} ${card.type.toLowerCase()}` + } else { + return card.brand === 'jcb' ? card.label.toUpperCase() : card.label + } +} + +const formatCardTypesForAdminTemplate = (allCards, acceptedCards, account) => { + const cardDataChecklistItem = (card) => { + return { + value: card.id, + text: formatLabel(card), + checked: acceptedCards.filter(accepted => accepted.id === card.id).length !== 0, + requires3ds: card.requires3ds + } + } + const disableCheckboxIf3dsRequiredButNotEnabled = (cardTypeChecklistItem) => { + if (cardTypeChecklistItem.requires3ds && !account.requires3ds) { + cardTypeChecklistItem.disabled = true + cardTypeChecklistItem.hint = { + html: account.type === 'test' ? `${cardTypeChecklistItem.text} is not available on test accounts` : `${cardTypeChecklistItem.text} cannot be used because 3D Secure is not available. Please contact support` + } + } + return cardTypeChecklistItem + } + const addHintForAmexAndUnionpay = (cardTypeChecklistItem) => { + if (['American Express', 'Union Pay'].includes(cardTypeChecklistItem.text)) { + if (account.paymentProvider === 'worldpay') { + cardTypeChecklistItem.hint = { + html: 'You must have already enabled this with Worldpay' + } + } + } + return cardTypeChecklistItem + } + const debitCardChecklistItems = allCards.filter(card => card.type === 'DEBIT') + .map(card => cardDataChecklistItem(card)) + .map(cardTypeChecklistItem => disableCheckboxIf3dsRequiredButNotEnabled(cardTypeChecklistItem)) + + const creditCardChecklistItems = allCards.filter(card => card.type === 'CREDIT') + .map(card => cardDataChecklistItem(card)) + .map(cardTypeChecklistItem => addHintForAmexAndUnionpay(cardTypeChecklistItem)) + + return { debitCards: debitCardChecklistItems, creditCards: creditCardChecklistItems } +} + +const formatCardTypesForNonAdminTemplate = (allCards, acceptedCards) => { + const acceptedCardTypeIds = acceptedCards.map(card => card.id) + const formattedCardTypes = { + 'Enabled debit cards': [], + 'Not enabled debit cards': [], + 'Enabled credit cards': [], + 'Not enabled credit cards': [] + } + allCards.forEach(card => { + const cardIsEnabled = acceptedCardTypeIds.includes(card.id) ? 'Enabled' : 'Not enabled' + formattedCardTypes[`${cardIsEnabled} ${card.type.toLowerCase()} cards`].push(formatLabel(card)) + }) + return formattedCardTypes +} + +const formatCardTypesForTemplate = (allCards, acceptedCards, account, isAdminUser) => { + if (isAdminUser) { + return formatCardTypesForAdminTemplate(allCards, acceptedCards, account) + } + return formatCardTypesForNonAdminTemplate(allCards, acceptedCards) +} + +module.exports = { + formatCardTypesForTemplate +} diff --git a/app/utils/simplified-account/format/format-card-types.test.js b/app/utils/simplified-account/format/format-card-types.test.js new file mode 100644 index 000000000..3e3d8ed87 --- /dev/null +++ b/app/utils/simplified-account/format/format-card-types.test.js @@ -0,0 +1,78 @@ +const { expect } = require('chai') +const { formatCardTypesForTemplate } = require('@utils/simplified-account/format/format-card-types') + +const allCards = [ + { + id: 'id-001', + brand: 'visa', + label: 'Visa', + type: 'DEBIT', + requires3ds: false + }, + { + id: 'id-002', + brand: 'visa', + label: 'Visa', + type: 'CREDIT', + requires3ds: false + }, + { + id: 'id-003', + brand: 'master-card', + label: 'Mastercard', + type: 'DEBIT', + requires3ds: false + }, + { + id: 'id-004', + brand: 'american-express', + label: 'American Express', + type: 'CREDIT', + requires3ds: false + }, + { + id: 'id-005', + brand: 'jcb', + label: 'Jcb', + type: 'CREDIT', + requires3ds: false + }, + { + id: 'id-006', + brand: 'maestro', + label: 'Maestro', + type: 'DEBIT', + requires3ds: true + } +] +describe('format-card-types for template', () => { + describe('present checkboxes for admin user', () => { + it('should return all card types if they are all enabled', () => { + const acceptedCards = [...allCards] + const account = { requires3ds: true } + const cards = formatCardTypesForTemplate(allCards, acceptedCards, account, true) + expect(cards.debitCards).to.have.length(3) + expect(cards.creditCards).to.have.length(3) + }) + + it('should set checkbox to disabled for requires3ds card types if 3ds not enabled on account', () => { + const acceptedCards = [...allCards] + const account = { requires3ds: false } + const cards = formatCardTypesForTemplate(allCards, acceptedCards, account, true) + expect(cards.debitCards.filter(card => card.disabled === true)).to.have.length(1) + expect(cards.debitCards.filter(card => card.disabled === true)[0]).to.have.property('text').to.equal('Maestro') + }) + }) + + describe('present read-only list for non-admin user', () => { + it('should return all card types arranged by type and whether accepted or not', () => { + const acceptedCards = [...allCards].filter(card => card.id !== 'id-001' && card.id !== 'id-002') + const account = { requires3ds: true } + const cards = formatCardTypesForTemplate(allCards, acceptedCards, account, false) + expect(cards['Enabled debit cards']).to.have.length(2).to.deep.equal(['Mastercard debit', 'Maestro']) + expect(cards['Not enabled debit cards']).to.have.length(1).to.deep.equal(['Visa debit']) + expect(cards['Enabled credit cards']).to.have.length(2).to.deep.equal(['American Express', 'JCB']) + expect(cards['Not enabled credit cards']).to.have.length(1).to.deep.equal(['Visa credit']) + }) + }) +}) diff --git a/app/views/simplified-account/settings/card-types/cardTypesCheckboxes.njk b/app/views/simplified-account/settings/card-types/cardTypesCheckboxes.njk new file mode 100644 index 000000000..8e2a14d7a --- /dev/null +++ b/app/views/simplified-account/settings/card-types/cardTypesCheckboxes.njk @@ -0,0 +1,44 @@ + +

Choose which credit and debit cards you want to accept.

+ +
+ + {{ + govukCheckboxes({ + idPrefix: "debit", + name: "debit", + classes: "with-border govuk-!-padding-top-3 pay-!-border-top", + fieldset: { + legend: { + text: "Debit cards", + classes: "govuk-fieldset__legend--s" + } + }, + items: cardTypes.debitCards + }) + }} + + {{ + govukCheckboxes({ + idPrefix: "credit", + name: "credit", + classes: "with-border govuk-!-padding-top-3 pay-!-border-top", + fieldset: { + legend: { + text: "Credit cards", + classes: "govuk-fieldset__legend--s" + } + }, + items: cardTypes.creditCards + }) + }} + + {{ + govukButton({ + text: 'Save changes', + attributes: { + id: "save-card-types" + } + }) + }} +
diff --git a/app/views/simplified-account/settings/card-types/cardTypesList.njk b/app/views/simplified-account/settings/card-types/cardTypesList.njk new file mode 100644 index 000000000..c481b71df --- /dev/null +++ b/app/views/simplified-account/settings/card-types/cardTypesList.njk @@ -0,0 +1,21 @@ +{% for category, items in cardTypes %} + {% if (items.length > 0) %} +

+ {{ category }} +

+ {% set cardList = [] %} + {% for card in items %} + {% set cardListItem = { + key: { + text: card, + classes: "govuk-!-display-none" + }, + value: { + html: card + } + } %} + {% set cardList = (cardList.push(cardListItem), cardList) %} + {% endfor %} + {{ govukSummaryList({ rows: cardList }) }} + {% endif %} +{% endfor %} diff --git a/app/views/simplified-account/settings/card-types/index.njk b/app/views/simplified-account/settings/card-types/index.njk index 447667725..ab11152a3 100644 --- a/app/views/simplified-account/settings/card-types/index.njk +++ b/app/views/simplified-account/settings/card-types/index.njk @@ -5,4 +5,20 @@ {% endblock %} {% block settingsContent %} -{% endblock %} + {% if not isAdminUser %} + {{ govukInsetText({ + text: "You don’t have permission to manage settings. Contact your service admin if you would like to manage 3D Secure, + accepted card types, email notifications, billing address or mask card numbers or security codes for MOTO services.", + classes: "service-settings-inset-text--grey" + }) }} + {% endif %} + +

Card types

+ + {% if isAdminUser %} + {% include "./cardTypesCheckboxes.njk" %} + {% else %} + {% include "./cardTypesList.njk" %} + {% endif %} + +{% endblock %} diff --git a/test/test-helpers/simplified-account/controllers/ControllerTestBuilder.class.js b/test/test-helpers/simplified-account/controllers/ControllerTestBuilder.class.js index b6750aff3..aa6367bb4 100644 --- a/test/test-helpers/simplified-account/controllers/ControllerTestBuilder.class.js +++ b/test/test-helpers/simplified-account/controllers/ControllerTestBuilder.class.js @@ -7,6 +7,7 @@ module.exports = class ControllerTestBuilder { this.controllerPath = controllerPath this.next = sinon.spy() this.req = { + user: {}, service: {}, account: {}, flash: sinon.spy() @@ -39,6 +40,11 @@ module.exports = class ControllerTestBuilder { return this } + withUser (user) { + this.req.user = user + return this + } + withStubs (stubs) { this.stubs = stubs return this