Skip to content

Commit

Permalink
PP 11877 Validate date range entry on the transaction search page (#4162
Browse files Browse the repository at this point in the history
)

When searching for transactions in SelfService between two dates, if the start date entered is later than the end date, no transactions are listed.
We should be rendering an error here to show the user the input mistake and how to rectify it.
This fix is done on both the transactions and the all services transactions search pages.
New cypress, non-cypress integration test and standard unit tests were written to cover the new validation functionality implemented.
  • Loading branch information
olatomgds authored Dec 20, 2023
1 parent c7b88ab commit 1e5f876
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 14 deletions.
2 changes: 1 addition & 1 deletion app/controllers/all-service-transactions/get.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ module.exports = async function getTransactionsForAllServices (req, res, next) {
const searchResultOutput = await transactionService.search(userPermittedAccountsSummary.gatewayAccountIds, filters.result)
const cardTypes = await client.getAllCardTypes()
const downloadRoute = filterLiveAccounts ? paths.allServiceTransactions.download : paths.formattedPathFor(paths.allServiceTransactions.downloadStatusFilter, 'test')
const model = buildPaymentList(searchResultOutput, cardTypes, null, filters.result, downloadRoute, req.session.backPath)
const model = buildPaymentList(searchResultOutput, cardTypes, null, filters.result,filters.dateRangeState, downloadRoute, req.session.backPath)
delete req.session.backPath
model.search_path = filterLiveAccounts ? paths.allServiceTransactions.index : paths.formattedPathFor(paths.allServiceTransactions.indexStatusFilter, 'test')
model.filtersDescription = describeFilters(filters.result)
Expand Down
31 changes: 31 additions & 0 deletions app/controllers/all-service-transactions/get.controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,37 @@ describe('All service transactions - GET', () => {
})
})

describe('Error results when from date is later than to date', () => {
const request = {
account: gatewayAccountFixture.validGatewayAccount({ 'payment_provider': 'stripe' }),
service: service,
user: user,
params: {},
query: {
fromDate: '03/5/2018',
fromTime: '01:00:00',
toDate: '01/5/2018',
toTime: '01:00:00'
},
url: 'http://selfservice/all-servce-transactions',
session: {}
}
const response = {
render: sinon.spy()
}

it('should return the response with the date-range failing validation with empty transaction results indicator', async () => {
await getController()(request, response, next)

sinon.assert.calledWith(response.render,'transactions/index',sinon.match({
'isInvalidDateRange': true,
'hasResults': false,
'fromDateParam': "03/5/2018",
'toDateParam': "01/5/2018",
}))
})
})

describe('Non stripe account', () => {
it('should NOT get dispute states', async () => {
userPermittedAccountsSummary.hasStripeAccount = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ const paths = require('../../paths')
const formatAccountPathsFor = require('../../utils/format-account-paths-for')
const { validGatewayAccountResponse } = require('../../../test/fixtures/gateway-account.fixtures')
const transactionListController = require('./transaction-list.controller')
const proxyquire = require("proxyquire");
const ledgerTransactionFixture = require("../../../test/fixtures/ledger-transaction.fixtures");
const gatewayAccountFixture = require("../../../test/fixtures/gateway-account.fixtures");
const Service = require("../../models/Service.class");
const serviceFixtures = require("../../../test/fixtures/service.fixtures");
const User = require("../../models/User.class");
const userFixtures = require("../../../test/fixtures/user.fixtures");

// Setup
const gatewayAccountId = '651342'
Expand All @@ -13,6 +20,8 @@ const requestId = 'unique-request-id'
const headers = { 'x-request-id': requestId }

describe('The /transactions endpoint', () => {
const transactionSearchResponse = ledgerTransactionFixture.validTransactionSearchResponse(
{ transactions: [] })
const account = validGatewayAccountResponse(
{
external_id: EXTERNAL_GATEWAY_ACCOUNT_ID,
Expand All @@ -21,6 +30,9 @@ describe('The /transactions endpoint', () => {
credentials: { 'username': 'a-username' }
}
)
const service = new Service(serviceFixtures.validServiceResponse({}))
const user = new User(userFixtures.validUserResponse())

const req = {
account,
headers,
Expand All @@ -45,6 +57,35 @@ describe('The /transactions endpoint', () => {
})
})

describe('Error results when from date is later than to date', () => {
const request = {
account: gatewayAccountFixture.validGatewayAccount({ 'payment_provider': 'stripe' }),
service: service,
user: user,
query: {
fromDate: '03/5/2018',
fromTime: '01:00:00',
toDate: '01/5/2018',
toTime: '01:00:00'
},
url: 'http://selfservice/servce-transactions',
session: {}
}
const response = {
render: sinon.spy()
}
it('should return the response with the date-range failing validation with empty transaction results indicator', async () => {
await getController()(request, response, next)

sinon.assert.calledWith(response.render,'transactions/index',sinon.match({
'isInvalidDateRange': true,
'hasResults': false,
'fromDateParam': "03/5/2018",
'toDateParam': "01/5/2018",
}))
})
})

describe('Pagination', () => {
it('should return return error if page out of bounds', async () => {
const reqWithInvalidPage = {
Expand All @@ -61,7 +102,7 @@ describe('The /transactions endpoint', () => {
sinon.assert.calledWith(next, expectedError)
})

it('should return return error if pageSize out of bounds 1', async () => {
it('should return error if pageSize out of bounds 1', async () => {
const reqWithInvalidPageSize = {
...req,
query: {
Expand All @@ -76,7 +117,7 @@ describe('The /transactions endpoint', () => {
sinon.assert.calledWith(next, expectedError)
})

it('should return return error if pageSize out of bounds 2', async () => {
it('should return error if pageSize out of bounds 2', async () => {
const reqWithInvalidPageSize = {
...req,
query: {
Expand All @@ -91,4 +132,16 @@ describe('The /transactions endpoint', () => {
sinon.assert.calledWith(next, expectedError)
})
})

function getController () {
return proxyquire('./transaction-list.controller', {
'../../services/transaction.service': {
search: sinon.spy(() => Promise.resolve(transactionSearchResponse))
},
'../../services/clients/connector.client.js': {
ConnectorClient: class {async getAllCardTypes () { return {} }}
}
})
}

})
19 changes: 12 additions & 7 deletions app/controllers/transactions/transaction-list.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,32 @@ const formatAccountPathsFor = require('../../utils/format-account-paths-for')
module.exports = async function showTransactionList (req, res, next) {
const accountId = req.account.gateway_account_id
const gatewayAccountExternalId = req.account.external_id

const filters = getFilters(req)

const EMPTY_TRANSACTION_SEARCH_RESULTS = {
total: 0,
count: 0,
page: 1,
results: [],
_links: {},
filters: {},
}
req.session.filters = url.parse(req.url).query

if (!filters.valid) {
return next(new Error('Invalid search'))
}

let result
let transactionSearchResults = (filters.dateRangeState.isInvalidDateRange) ?
EMPTY_TRANSACTION_SEARCH_RESULTS : transactionService.search([accountId], filters.result)
try {
result = await Promise.all([
transactionService.search([accountId], filters.result),
client.getAllCardTypes()
])
result = await Promise.all([ transactionSearchResults, client.getAllCardTypes() ])
} catch (error) {
return next(error)
}

const transactionsDownloadLink = formatAccountPathsFor(router.paths.account.transactions.download, req.account.external_id)
const model = buildPaymentList(result[0], result[1], gatewayAccountExternalId, filters.result, transactionsDownloadLink)
const model = buildPaymentList(result[0], result[1], gatewayAccountExternalId, filters.result, filters.dateRangeState, transactionsDownloadLink)
model.search_path = formatAccountPathsFor(router.paths.account.transactions.index, req.account.external_id)
model.filtersDescription = describeFilters(filters.result)

Expand Down
19 changes: 18 additions & 1 deletion app/utils/filters.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const qs = require('qs')
const moment = require('moment-timezone')
const check = require('check-types')
const Paginator = require('../utils/paginator.js')
const states = require('../utils/states')
Expand All @@ -24,6 +25,20 @@ function trimFilterValues (filters) {
return filters
}

function validateDateRange(filters){
const result = moment(filters.fromDate, 'DD/MM/YYYY').isAfter(moment(filters.toDate, 'DD/MM/YYYY'))? 1: -1;
let isInvalid = false

if (result === 1) {
isInvalid = true
}
return {
isInvalidDateRange: isInvalid,
fromDateParam: filters.fromDate,
toDateParam: filters.toDate
}
}

function getFilters (req) {
let filters = trimFilterValues(qs.parse(req.query))
filters.selectedStates = []
Expand All @@ -41,6 +56,7 @@ function getFilters (req) {

filters = _.omitBy(filters, _.isEmpty)
return {
dateRangeState: validateDateRange(filters),
valid: validateFilters(filters),
result: filters
}
Expand Down Expand Up @@ -75,5 +91,6 @@ function describeFilters (filters) {

module.exports = {
getFilters: getFilters,
describeFilters: describeFilters
describeFilters: describeFilters,
validateDateRange: validateDateRange
}
13 changes: 13 additions & 0 deletions app/utils/filters.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,18 @@ describe('filters', () => {
expect(result.trim()).to.equal('from <strong>from-date</strong> to <strong>to-date</strong>')
})
})

describe('validateDateRange', () => {
it('should validate the date range filter and return an array with the result and the associated from and to date', function () {
const testFilter = {
fromDate: '03/03/2023',
toDate: '01/03/2023'
}
const result = filters.validateDateRange(testFilter)
expect(result.isInvalidDateRange).to.equal(true)
expect(result.fromDateParam).to.equal('03/03/2023')
expect(result.toDateParam).to.equal('01/03/2023')
})
})
})
})
9 changes: 7 additions & 2 deletions app/utils/transaction-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const LEDGER_TRANSACTION_COUNT_LIMIT = 5000

module.exports = {
/** prepares the transaction list view */
buildPaymentList: function (connectorData, allCards, gatewayAccountExternalId, filtersResult, route, backPath) {
buildPaymentList: function (connectorData, allCards, gatewayAccountExternalId, filtersResult, filtersDateRangeState, route, backPath) {
connectorData.filters = filtersResult
connectorData.hasFilters = Object.keys(filtersResult).length !== 0
connectorData.hasResults = connectorData.results.length !== 0
Expand All @@ -31,10 +31,15 @@ module.exports = {
connectorData.maxLimitFormatted = parseInt(LEDGER_TRANSACTION_COUNT_LIMIT).toLocaleString()
connectorData.paginationLinks = getPaginationLinks(connectorData)
connectorData.hasPaginationLinks = !!getPaginationLinks(connectorData)

connectorData.hasPageSizeLinks = hasPageSizeLinks(connectorData)
connectorData.pageSizeLinks = getPageSizeLinks(connectorData)

if(filtersDateRangeState){
connectorData.isInvalidDateRange = filtersDateRangeState.isInvalidDateRange === true
connectorData.fromDateParam = filtersDateRangeState.fromDateParam
connectorData.toDateParam = filtersDateRangeState.toDateParam
}

connectorData.cardBrands = lodash.uniqBy(allCards.card_types, 'brand')
.map(card => {
return {
Expand Down
5 changes: 5 additions & 0 deletions app/views/transactions/index.njk
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,17 @@
{% include "transactions/display-size.njk" %}

<h3 class="govuk-heading-s govuk-!-font-weight-regular govuk-!-margin-top-3" id="total-results">
{% if (isInvalidDateRange) %}
<p> End date must be after start date </p>
<p> The from date entered is <span class="govuk-body govuk-!-font-weight-bold">{{fromDateParam}}</span> and the to date entered is <span class="govuk-body govuk-!-font-weight-bold">{{toDateParam}}</span></p>
{% else %}
{% if totalOverLimit %}
Over {{maxLimitFormatted}} transactions
{% else %}
{{totalFormatted}} transactions
{% endif %}
{{ filtersDescription | safe }}
{% endif %}
</h3>

{% if allServiceTransactions or permissions.transactions_download_read %}
Expand Down
7 changes: 6 additions & 1 deletion app/views/transactions/list.njk
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,10 @@
</tbody>
</table>
{% else %}
<p class="govuk-body" id="no-results">There are no results for the filters you used.</p>
{% if not isInvalidDateRange %}
<p class="govuk-body" id="no-results">There are no results for the filters you used.</p>
{% endif %}
{% endif %}



45 changes: 45 additions & 0 deletions test/cypress/integration/transactions/transaction-search.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,51 @@ describe('Transactions List', () => {
assertTransactionRow(1, filteredByMultipleFieldsTransactions[1].reference, `/account/${gatewayAccountExternalId}/transactions/payment-transaction-id2`,
'[email protected]', '–£15.00', 'Visa', 'Refund submitted')
})

it('should display error message when searching with from date later than to date', () => {
cy.task('setupStubs', [
...sharedStubs(),
transactionsStubs.getLedgerTransactionsSuccess({ gatewayAccountId, transactions: unfilteredTransactions }),
transactionsStubs.getLedgerTransactionsSuccess({
gatewayAccountId,
transactions: filteredByDatesTransactions,
filters: {
from_date: '2018-05-03T00:00:00.000Z',
to_date: '2018-05-03T00:00:01.000Z'
}
})
])
cy.visit(transactionsUrl)

cy.get('.datepicker').should('not.exist')
cy.get('.ui-timepicker-wrapper').should('not.exist')

cy.get('#fromDate').type('03/5/2018')

cy.get('.datepicker').should('be.visible')
cy.get('.ui-timepicker-wrapper').should('not.exist')

cy.get('#fromTime').type('01:00:00')

cy.get('.datepicker').should('not.exist')
cy.get('.ui-timepicker-wrapper').should('be.visible')

cy.get('#toDate').type('02/5/2018')

cy.get('.datepicker').should('be.visible')
cy.get('.ui-timepicker-wrapper').should('not.be.visible')

cy.get('#toTime').type('01:00:00')

cy.get('.datepicker').should('not.exist')
cy.get('.ui-timepicker-wrapper').should('be.visible')

cy.get('#filter').click()

cy.get('#transactions-list tbody').should('not.exist')

cy.get('h3').contains('End date must be after start date' )
})
})
describe('Transactions are displayed correctly', () => {
it('should display card fee with corporate card surcharge transaction', () => {
Expand Down

0 comments on commit 1e5f876

Please sign in to comment.