From 552bdefb0b760fdcf715051ae51ea41637c9c36a Mon Sep 17 00:00:00 2001 From: "ola.tomoloju" Date: Tue, 14 Nov 2023 08:18:59 +0000 Subject: [PATCH] PP-11288-Improve Validation on Prefilled Payment Links - Updated the session data handling logic which determined a prefilled journey and used the paymentLinkSession attribute - Added test for the amount page for non-prefilled payment link scenario. - Added test for the confirmation page prefilled payment link scenario. --- app/payment-links/amount/amount.controller.js | 2 - .../amount/amount.controller.test.js | 26 ++++++ .../confirm/confirm.controller.js | 14 ++-- .../confirm/confirm.controller.test.js | 83 +++++++++++++------ .../utils/payment-link-session.js | 12 +-- locales/en.json | 4 +- 6 files changed, 95 insertions(+), 46 deletions(-) diff --git a/app/payment-links/amount/amount.controller.js b/app/payment-links/amount/amount.controller.js index 80b278cf5..93c17383a 100644 --- a/app/payment-links/amount/amount.controller.js +++ b/app/payment-links/amount/amount.controller.js @@ -73,8 +73,6 @@ function postPage (req, res, next) { const paymentAmountInPence = convertPoundsAndPenceToPence(paymentAmount) paymentLinkSession.setAmount(req, product.externalId, paymentAmountInPence) - paymentLinkSession.setPrefilledFlag(req, product.externalId, 'false') - return res.redirect(replaceParamsInPath(paths.paymentLinks.confirm, product.externalId)) } diff --git a/app/payment-links/amount/amount.controller.test.js b/app/payment-links/amount/amount.controller.test.js index 772f07243..49e81e182 100644 --- a/app/payment-links/amount/amount.controller.test.js +++ b/app/payment-links/amount/amount.controller.test.js @@ -234,6 +234,32 @@ describe('Amount Page Controller', () => { sinon.assert.calledWith(res.locals.__p, 'paymentLinks.fieldValidation.enterAnAmountInPounds') }) + it('when a zero value amount is entered, it should display an error message and the back link correctly', () => { + req = { + correlationId: '123', + product, + body: { + 'payment-amount': '0.00' + } + } + + res = { + redirect: sinon.spy(), + locals: { + __p: sinon.spy() + } + } + + controller.postPage(req, res) + + sinon.assert.calledWith(responseSpy, req, res, 'amount/amount') + + const pageData = mockResponses.response.args[0][3] + expect(pageData.backLinkHref).to.equal('/pay/an-external-id/reference') + + sinon.assert.calledWith(res.locals.__p, 'paymentLinks.fieldValidation.enterANonZeroAmountInPounds') + }) + it('when an invalid amount is entered and the change query parameter is present, it should display an error' + 'message and set the back link to the CONFIRM page', () => { req = { diff --git a/app/payment-links/confirm/confirm.controller.js b/app/payment-links/confirm/confirm.controller.js index bbfec88a9..68b20900e 100644 --- a/app/payment-links/confirm/confirm.controller.js +++ b/app/payment-links/confirm/confirm.controller.js @@ -10,7 +10,7 @@ const captcha = require('../../utils/captcha') const logger = require('../../utils/logger')(__filename) const productsClient = require('../../clients/products/products.client') const paymentLinkSession = require('../utils/payment-link-session') -const { validateAmount } = require('../../utils/validation/form-validations') +const formValidation = require('../../utils/validation/form-validations') const { convertPenceToPoundsAndPence } = require('../../utils/currency') const { AccountCannotTakePaymentsError } = require('../../errors') @@ -18,6 +18,7 @@ const HIDDEN_FORM_FIELD_ID_REFERENCE_VALUE = 'reference-value' const HIDDEN_FORM_FIELD_ID_AMOUNT = 'amount' const GOOGLE_RECAPTCHA_FORM_NAME = 'g-recaptcha-response' const ERROR_KEY_RECAPTCHA = 'recaptcha' +const CONTACT_SERVICE_ZERO_VALUE_PAYMENT_ERROR = 'error.contactServiceForZeroValuePayment' function getBackLinkUrl (product, referenceProvidedByQueryParams, amountProvidedByQueryParams) { if (!product.price && !amountProvidedByQueryParams) { @@ -106,13 +107,12 @@ async function postPage (req, res, next) { const sessionAmount = paymentLinkSession.getAmount(req, product.externalId) const referenceProvidedByQueryParams = paymentLinkSession.getReferenceProvidedByQueryParams(req, product.externalId) const amountProvidedByQueryParams = paymentLinkSession.getAmountProvidedByQueryParams(req, product.externalId) - const isPrefilledPayment = paymentLinkSession.getPrefilledFlag(req, product.externalId) - if (isPrefilledPayment !== 'false' && - validateAmount(amountProvidedByQueryParams) && - !validateAmount(amountProvidedByQueryParams).valid) { - const zeroAmountErrorMessagePath = 'error.contactServiceForZeroValuePayment' - return renderErrorView(req, res, zeroAmountErrorMessagePath, 400) + if (amountProvidedByQueryParams && + formValidation.validateAmount(sessionAmount.toString()) && + !formValidation.validateAmount(sessionAmount.toString()).valid + ) { + return renderErrorView(req, res, CONTACT_SERVICE_ZERO_VALUE_PAYMENT_ERROR, 400) } if (product.requireCaptcha) { diff --git a/app/payment-links/confirm/confirm.controller.test.js b/app/payment-links/confirm/confirm.controller.test.js index 7bb5c98d9..30c403757 100644 --- a/app/payment-links/confirm/confirm.controller.test.js +++ b/app/payment-links/confirm/confirm.controller.test.js @@ -521,37 +521,72 @@ describe('Confirm Page Controller', () => { sinon.assert.calledWith(next, expectedError) }) - it('when creating a payment returns a 403, should call next() with an AccountCannotTakePaymentsError', async () => { - req = { - correlationId: '123', - product, - body: { - amount: '1000' + it('when creating a prefilled payment link with an amount equals to £0.00, should render an error view with the correct message', + async () => { + req = { + correlationId: '123', + product, + body: { + amount: '0' + } } - } - res = { - redirect: sinon.stub() - } + res = { + setHeader: sinon.stub(), + status: sinon.stub(), + render: sinon.stub(), + locals: { + __p: sinon.stub() + } + } - const next = sinon.stub() + const next = sinon.stub() - const error = new Error('Failed to create payment.') - error.errorCode = 403 - mockProductsClient.payment.create.rejects(error) + mockPaymentLinkSession.getReference.withArgs(req, product.externalId).returns('test invoice number') + mockPaymentLinkSession.getAmount.withArgs(req, product.externalId).returns(0) + mockPaymentLinkSession.getReferenceProvidedByQueryParams.withArgs(req, product.externalId).returns(true) + mockPaymentLinkSession.getAmountProvidedByQueryParams.withArgs(req, product.externalId).returns(true) + res.locals.__p.withArgs('paymentLinks.confirm.totalToPay').returns('Total to pay') - await controller.postPage(req, res, next) + await controller.postPage(req, res, next) - sinon.assert.calledWith( - mockProductsClient.payment.create, - 'an-external-id', - 1000 - ) + sinon.assert.calledWith(res.setHeader, 'Content-Type', 'text/html') + sinon.assert.calledWith(res.status, 400) + sinon.assert.calledWith(res.render, 'error', { message: 'error.contactServiceForZeroValuePayment' }) + }) - const expectedError = sinon.match.instanceOf(AccountCannotTakePaymentsError) - .and(sinon.match.has('message', 'Forbidden response returned by Public API when creating payment')) - sinon.assert.calledWith(next, expectedError) - }) + it('when creating a payment returns a 403, should call next() with an AccountCannotTakePaymentsError', + async () => { + req = { + correlationId: '123', + product, + body: { + amount: '1000' + } + } + + res = { + redirect: sinon.stub() + } + + const next = sinon.stub() + + const error = new Error('Failed to create payment.') + error.errorCode = 403 + mockProductsClient.payment.create.rejects(error) + + await controller.postPage(req, res, next) + + sinon.assert.calledWith( + mockProductsClient.payment.create, + 'an-external-id', + 1000 + ) + + const expectedError = sinon.match.instanceOf(AccountCannotTakePaymentsError) + .and(sinon.match.has('message', 'Forbidden response returned by Public API when creating payment')) + sinon.assert.calledWith(next, expectedError) + }) it('should return 400 and redirect to reference page for CARD_NUMBER_IN_PAYMENT_LINK_REFERENCE_REJECTED error', async () => { req = { diff --git a/app/payment-links/utils/payment-link-session.js b/app/payment-links/utils/payment-link-session.js index 031b70e4b..480abe8b2 100644 --- a/app/payment-links/utils/payment-link-session.js +++ b/app/payment-links/utils/payment-link-session.js @@ -8,7 +8,6 @@ const AMOUNT_KEY = 'amount' const REFERENCE_PROVIDED_BY_QUERY_PARAMS_KEY = 'referenceProvidedByQueryParams' const AMOUNT_PROVIDED_BY_QUERY_PARAMS_KEY = 'amountProvidedByQueryParams' const ERROR_KEY = 'error' -const PRE_FILLED_KEY = 'prefilled' function cookieIndex (key, productExternalId) { return `${getSessionCookieName()}.${productExternalId}.${key}` @@ -56,13 +55,6 @@ function deletePaymentLinkSession (req, productExternalId) { lodash.unset(req, `${getSessionCookieName()}.${productExternalId}`) } -function getPrefilledFlag (req, productExternalId) { - return lodash.get(req, cookieIndex(PRE_FILLED_KEY, productExternalId)) -} -function setPrefilledFlag (req, productExternalId, prefilled) { - lodash.set(req, cookieIndex(PRE_FILLED_KEY, productExternalId), prefilled) -} - module.exports = { getReference, setReference, @@ -72,7 +64,5 @@ module.exports = { getAmountProvidedByQueryParams, deletePaymentLinkSession, getError, - setError, - setPrefilledFlag, - getPrefilledFlag + setError } diff --git a/locales/en.json b/locales/en.json index b5c167c04..2fc8314e7 100644 --- a/locales/en.json +++ b/locales/en.json @@ -19,7 +19,7 @@ "default": "There’s a problem with the payments platform. Please try again later", "internal": "Sorry, we’re unable to process your request. Try again later.", "contactService": "Please contact the service you are trying to make a payment to.", - "contactServiceForZeroValuePayment": "Please contact the service regarding the zero amount payment error." + "contactServiceForZeroValuePayment": "There is a problem with the link you have been sent to use to pay. The payment amount cannot be £0.00. Contact the service you’re trying to make a payment to." }, "404Page": { "title": "Page not found", @@ -82,7 +82,7 @@ }, "fieldValidation": { "enterAnAmountInPounds": "Enter an amount of money to pay", - "enterANonZeroAmountInPounds": "Enter an amount that is £0.01 or more", + "enterANonZeroAmountInPounds": "Amount must be £0.01 or more", "enterAnAmountInTheCorrectFormat": "Enter an amount in pounds and pence using digits and a decimal point, like 123.45 or 156.00", "enterAnAmountUnderMaxAmount": "Enter an amount that is £100,000 or less", "enterAReference": "You must enter your %s",