diff --git a/app/controllers/notifications-setup.controller.js b/app/controllers/notifications-setup.controller.js index 8733a68794..ac8e8c6cdc 100644 --- a/app/controllers/notifications-setup.controller.js +++ b/app/controllers/notifications-setup.controller.js @@ -1,17 +1,33 @@ 'use strict' -const ReturnsPeriodService = require('../services/notifications/setup/returns-period.service.js') -const ReviewService = require('../services/notifications/setup/review.service.js') -const SubmitReturnsPeriodService = require('../services/notifications/setup/submit-returns-period.service.js') -const InitiateSessionService = require('../services/notifications/setup/initiate-session.service.js') - /** * Controller for /notifications/setup endpoints * @module NotificationsSetupController */ +const DownloadRecipientsService = require('../services/notifications/setup/download-recipients.service.js') +const InitiateSessionService = require('../services/notifications/setup/initiate-session.service.js') +const ReturnsPeriodService = require('../services/notifications/setup/returns-period.service.js') +const ReviewService = require('../services/notifications/setup/review.service.js') +const SubmitReturnsPeriodService = require('../services/notifications/setup/submit-returns-period.service.js') + const basePath = 'notifications/setup' +async function downloadRecipients(request, h) { + const { + params: { sessionId } + } = request + + const { data, type, filename } = await DownloadRecipientsService.go(sessionId) + + return h + .response(data) + .type(type) + .encoding('binary') + .header('Content-Type', type) + .header('Content-Disposition', `attachment; filename="${filename}"`) +} + async function viewReturnsPeriod(request, h) { const { params: { sessionId } @@ -55,6 +71,7 @@ async function submitReturnsPeriod(request, h) { } module.exports = { + downloadRecipients, viewReturnsPeriod, viewReview, setup, diff --git a/app/controllers/return-logs-setup.controller.js b/app/controllers/return-logs-setup.controller.js index 49cfbc4380..b071969a66 100644 --- a/app/controllers/return-logs-setup.controller.js +++ b/app/controllers/return-logs-setup.controller.js @@ -11,6 +11,7 @@ const InitiateSessionService = require('../services/return-logs/setup/initiate-s const MeterDetailsService = require('../services/return-logs/setup/meter-details.service.js') const MeterProvidedService = require('../services/return-logs/setup/meter-provided.service.js') const NoteService = require('../services/return-logs/setup/note.service.js') +const PeriodUsedService = require('../services/return-logs/setup/period-used.service.js') const ReceivedService = require('../services/return-logs/setup/received.service.js') const ReportedService = require('../services/return-logs/setup/reported.service.js') const SingleVolumeService = require('../services/return-logs/setup/single-volume.service.js') @@ -18,6 +19,7 @@ const SubmissionService = require('../services/return-logs/setup/submission.serv const SubmitMeterDetailsService = require('../services/return-logs/setup/submit-meter-details.service.js') const SubmitMeterProvidedService = require('../services/return-logs/setup/submit-meter-provided.service.js') const SubmitNoteService = require('../services/return-logs/setup/submit-note.service.js') +const SubmitPeriodUsedService = require('../services/return-logs/setup/submit-period-used.service.js') const SubmitReceivedService = require('../services/return-logs/setup/submit-received.service.js') const SubmitReportedService = require('../services/return-logs/setup/submit-reported.service.js') const SubmitSingleVolumeService = require('../services/return-logs/setup/submit-single-volume.service.js') @@ -66,6 +68,13 @@ async function note(request, h) { return h.view('return-logs/setup/note.njk', pageData) } +async function periodUsed(request, h) { + const { sessionId } = request.params + const pageData = await PeriodUsedService.go(sessionId) + + return h.view('return-logs/setup/period-used.njk', pageData) +} + async function received(request, h) { const { sessionId } = request.params const pageData = await ReceivedService.go(sessionId) @@ -148,6 +157,18 @@ async function submitNote(request, h) { return h.redirect(`/system/return-logs/setup/${sessionId}/check`) } +async function submitPeriodUsed(request, h) { + const { sessionId } = request.params + + const pageData = await SubmitPeriodUsedService.go(sessionId, request.payload) + + if (pageData.error) { + return h.view('return-logs/setup/period-used.njk', pageData) + } + + return h.redirect(`/system/return-logs/setup/${sessionId}/check`) +} + async function submitReceived(request, h) { const { params: { sessionId }, @@ -196,7 +217,7 @@ async function submitSingleVolume(request, h) { } if (pageData.singleVolume === 'no') { - return h.redirect(`/system/return-logs/setup/${sessionId}/check-answers`) + return h.redirect(`/system/return-logs/setup/${sessionId}/check`) } return h.redirect(`/system/return-logs/setup/${sessionId}/period-used`) @@ -243,6 +264,7 @@ module.exports = { meterDetails, meterProvided, note, + periodUsed, received, reported, setup, @@ -251,6 +273,7 @@ module.exports = { submitMeterDetails, submitMeterProvided, submitNote, + submitPeriodUsed, submitReceived, submitReported, submitSingleVolume, diff --git a/app/controllers/return-logs.controller.js b/app/controllers/return-logs.controller.js index 3d1e1c42f3..874b265958 100644 --- a/app/controllers/return-logs.controller.js +++ b/app/controllers/return-logs.controller.js @@ -7,6 +7,8 @@ const Boom = require('@hapi/boom') +const SubmitViewReturnLogService = require('../services/return-logs/submit-view-return-log.service.js') + const ViewReturnLogService = require('../services/return-logs/view-return-log.service.js') async function view(request, h) { @@ -18,11 +20,20 @@ async function view(request, h) { const version = query.version ? Number(query.version) : 0 - const pageData = await ViewReturnLogService.go(query.id, version, auth) + const pageData = await ViewReturnLogService.go(query.id, version, auth, request.yar) return h.view('return-logs/view.njk', pageData) } +async function submitView(request, h) { + const { id } = request.query + + await SubmitViewReturnLogService.go(id, request.yar, request.payload) + + return h.redirect(`/system/return-logs?id=${id}`) +} + module.exports = { + submitView, view } diff --git a/app/presenters/notifications/setup/download-recipients.presenter.js b/app/presenters/notifications/setup/download-recipients.presenter.js new file mode 100644 index 0000000000..c1bb5454d1 --- /dev/null +++ b/app/presenters/notifications/setup/download-recipients.presenter.js @@ -0,0 +1,96 @@ +'use strict' + +/** + * Formats data for the `/notifications/setup/download` link + * @module DownloadRecipientsPresenter + */ + +const { contactName } = require('../../crm.presenter.js') +const { formatDateObjectToISO } = require('../../../lib/dates.lib.js') +const { transformArrayToCSVRow } = require('../../../lib/transform-to-csv.lib.js') + +const HEADERS = [ + 'Licences', + 'Return references', + 'Returns period start date', + 'Returns period end date', + 'Returns due date', + 'Message type', + 'Message reference', + 'Email', + 'Recipient name', + 'Address line 1', + 'Address line 2', + 'Address line 3', + 'Address line 4', + 'Address line 5', + 'Address line 6', + 'Postcode' +] + +/** + * Formats data for the `/notifications/setup/download` link. + * + * This function takes an array of recipient objects and transforms it into a CSV + * string suitable for download. + * + * The headers are fixed and in the correct order. If a value for a row does not match the header then it will default + * to an empty string. + * + * @param {object[]} recipients - An array of recipients + * + * @returns {string} - A CSV-formatted string that includes the recipients' data, with the first + * row as column headers and subsequent rows corresponding to the recipient details. + */ +function go(recipients) { + const rows = _transformToCsv(recipients) + + return [HEADERS + '\n', ...rows].join('') +} + +function _address(contact) { + if (!contact) { + return ['', '', '', '', '', '', ''] + } + + return [ + contact.addressLine1, + contact.addressLine2, + contact.addressLine3, + contact.addressLine4, + contact.town || contact.county, + contact.country, + contact.postcode + ] +} +/** + * Transforms the recipients' data into a CSV-compatible format. + * + * The order of the object dictates the CSV header order. + * + * @private + */ +function _transformToCsv(recipients) { + return recipients.map((recipient) => { + const { contact } = recipient + + const row = [ + recipient.licence_ref, + recipient.return_reference, + formatDateObjectToISO(recipient.start_date), + formatDateObjectToISO(recipient.end_date), + formatDateObjectToISO(recipient.due_date), + contact ? 'letter' : 'email', + 'invitations', + recipient.email || '', + contact ? contactName(recipient.contact) : '', + ..._address(contact) + ] + + return transformArrayToCSVRow(row) + }) +} + +module.exports = { + go +} diff --git a/app/presenters/return-logs/setup/period-used.presenter.js b/app/presenters/return-logs/setup/period-used.presenter.js new file mode 100644 index 0000000000..897b55ae67 --- /dev/null +++ b/app/presenters/return-logs/setup/period-used.presenter.js @@ -0,0 +1,52 @@ +'use strict' + +/** + * Format data for the `/return-log/setup/{sessionId}/period-used` page + * @module PeriodUsedPresenter + */ + +const { formatAbstractionPeriod } = require('../../base.presenter.js') + +/** + * Format data for the `/return-log/setup/{sessionId}/period-used` page + * + * @param {module:SessionModel} session - The return log setup session instance + * + * @returns {object} page data needed by the view template + */ +function go(session) { + const { + id: sessionId, + periodStartDay, + periodStartMonth, + periodEndDay, + periodEndMonth, + periodDateUsedOptions, + returnReference, + periodUsedFromDay, + periodUsedFromMonth, + periodUsedFromYear, + periodUsedToDay, + periodUsedToMonth, + periodUsedToYear + } = session + + return { + abstractionPeriod: formatAbstractionPeriod(periodStartDay, periodStartMonth, periodEndDay, periodEndMonth), + backLink: `/system/return-logs/setup/${sessionId}/single-volume`, + pageTitle: 'What period was used for this volume?', + periodDateUsedOptions: periodDateUsedOptions ?? null, + periodUsedFromDay: periodUsedFromDay ?? null, + periodUsedFromMonth: periodUsedFromMonth ?? null, + periodUsedFromYear: periodUsedFromYear ?? null, + periodUsedToDay: periodUsedToDay ?? null, + periodUsedToMonth: periodUsedToMonth ?? null, + periodUsedToYear: periodUsedToYear ?? null, + returnReference, + sessionId + } +} + +module.exports = { + go +} diff --git a/app/presenters/return-logs/view-return-log.presenter.js b/app/presenters/return-logs/view-return-log.presenter.js index 5cc784265d..1d8aa39550 100644 --- a/app/presenters/return-logs/view-return-log.presenter.js +++ b/app/presenters/return-logs/view-return-log.presenter.js @@ -40,6 +40,7 @@ function go(returnLog, auth) { status, startDate, twoPartTariff, + underQuery, versions } = returnLog @@ -74,6 +75,7 @@ function go(returnLog, auth) { tableTitle: _tableTitle(returnsFrequency, method), tariff: twoPartTariff ? 'Two-part' : 'Standard', total: _total(selectedReturnSubmission), + underQuery, versions: _versions(selectedReturnSubmission, versions, id) } } diff --git a/app/routes/notifications-setup.routes.js b/app/routes/notifications-setup.routes.js index c6e9fde31e..d37249c4cb 100644 --- a/app/routes/notifications-setup.routes.js +++ b/app/routes/notifications-setup.routes.js @@ -17,6 +17,18 @@ const routes = [ } } }, + { + method: 'GET', + path: basePath + '/{sessionId}/download', + options: { + handler: NotificationsSetupController.downloadRecipients, + auth: { + access: { + scope: ['returns'] + } + } + } + }, { method: 'GET', path: basePath + '/{sessionId}/returns-period', diff --git a/app/routes/return-logs-setup.routes.js b/app/routes/return-logs-setup.routes.js index 2b2132ce9d..485a67d0b3 100644 --- a/app/routes/return-logs-setup.routes.js +++ b/app/routes/return-logs-setup.routes.js @@ -242,6 +242,30 @@ const routes = [ } } } + }, + { + method: 'GET', + path: '/return-logs/setup/{sessionId}/period-used', + options: { + handler: ReturnLogsSetupController.periodUsed, + auth: { + access: { + scope: ['billing'] + } + } + } + }, + { + method: 'POST', + path: '/return-logs/setup/{sessionId}/period-used', + options: { + handler: ReturnLogsSetupController.submitPeriodUsed, + auth: { + access: { + scope: ['billing'] + } + } + } } ] diff --git a/app/routes/return-logs.routes.js b/app/routes/return-logs.routes.js index 914c8e9ff3..ef092e6fed 100644 --- a/app/routes/return-logs.routes.js +++ b/app/routes/return-logs.routes.js @@ -9,6 +9,13 @@ const routes = [ options: { handler: ReturnLogsController.view } + }, + { + method: 'POST', + path: '/return-logs', + options: { + handler: ReturnLogsController.submitView + } } ] diff --git a/app/services/notifications/setup/download-recipients.service.js b/app/services/notifications/setup/download-recipients.service.js new file mode 100644 index 0000000000..225c383b72 --- /dev/null +++ b/app/services/notifications/setup/download-recipients.service.js @@ -0,0 +1,45 @@ +'use strict' + +/** + * Orchestrates fetching and formatting the data needed for the notifications setup download link + * @module DownloadRecipientsService + */ + +const DetermineReturnsPeriodService = require('./determine-returns-period.service.js') +const DownloadRecipientsPresenter = require('../../../presenters/notifications/setup/download-recipients.presenter.js') +const FetchDownloadRecipientsService = require('./fetch-download-recipients.service.js') +const SessionModel = require('../../../models/session.model.js') + +/** + * Orchestrates fetching and formatting the data needed for the notifications setup download link + * + * This service creates a csv file of recipient for the user to download. It does not seem necessary to use a `Stream` + * to create the csv as the data is relatively small. + * + * @param {string} sessionId - The UUID for setup ad-hoc returns notification session record + * + * @returns {Promise} The data for the download link (csv string, filename and type) + */ +async function go(sessionId) { + const session = await SessionModel.query().findById(sessionId) + const { notificationType, referenceCode, returnsPeriod } = session + + const determinedReturnsPeriod = DetermineReturnsPeriodService.go(returnsPeriod) + + const recipients = await FetchDownloadRecipientsService.go( + determinedReturnsPeriod.returnsPeriod.dueDate, + determinedReturnsPeriod.summer + ) + + const formattedData = DownloadRecipientsPresenter.go(recipients) + + return { + data: formattedData, + type: 'text/csv', + filename: `${notificationType} - ${referenceCode}.csv` + } +} + +module.exports = { + go +} diff --git a/app/services/notifications/setup/fetch-download-recipients.service.js b/app/services/notifications/setup/fetch-download-recipients.service.js index 9e9ce9d9ae..2b76e75fd3 100644 --- a/app/services/notifications/setup/fetch-download-recipients.service.js +++ b/app/services/notifications/setup/fetch-download-recipients.service.js @@ -44,7 +44,7 @@ const { db } = require('../../../../db/db.js') * different contacts types (but still preferring the registered over unregistered licence). * * @param {Date} dueDate - * @param {boolean} summer + * @param {string} summer * * @returns {Promise} - matching recipients */ diff --git a/app/services/notifications/setup/initiate-session.service.js b/app/services/notifications/setup/initiate-session.service.js index e404a647b2..01ae92fff8 100644 --- a/app/services/notifications/setup/initiate-session.service.js +++ b/app/services/notifications/setup/initiate-session.service.js @@ -1,29 +1,68 @@ 'use strict' /** - * Initiates the session record used for setting up a new ad-hoc returns notification + * Initiates the session record used for setting up a new returns notification * @module InitiateSessionService */ const SessionModel = require('../../../models/session.model.js') +const NOTIFICATION_TYPES = { + invitation: { + prefix: 'RINV-', + type: 'Returns invitation' + }, + reminder: { + prefix: 'RREM-', + type: 'Returns reminder' + } +} + /** - * Initiates the session record used for setting up a new ad-hoc returns notification + * Initiates the session record used for setting up a new returns notification * - * During the setup journey for a new ad-hoc returns notification we temporarily store the data in a `SessionModel` + * During the setup journey for a new returns notification we temporarily store the data in a `SessionModel` * instance. It is expected that on each page of the journey the GET will fetch the session record and use it to * populate the view. * When the page is submitted the session record will be updated with the next piece of data. * - * At the end when the journey is complete the data from the session will be used to create the ad-hoc returns + * At the end when the journey is complete the data from the session will be used to create the returns * notification and the session record itself deleted. * + * This session will be used for both types of notifications (invitations and reminders). We set the prefix and type + * for the upstream services to use e.g. the prefix and code are used in the filename of a csv file. + * * @returns {Promise} the newly created session record */ async function go() { - // NOTE: data defaults to {} when a new record is created. But Objection.js throws a 'The query is empty' if we pass - // nothing into `insert()`. - return SessionModel.query().insert({ data: {} }).returning('id') + const { prefix, type } = NOTIFICATION_TYPES['invitation'] + + return SessionModel.query() + .insert({ + data: { + referenceCode: _generateReferenceCode(prefix), + notificationType: type + } + }) + .returning('id') +} + +/** + * A function to generate a pseudo-unique reference code for recipients notifications + * + * @param {string} prefix + * + * @returns {string} A reference code with a prefix and random string (RINV-A14GB8) + */ +function _generateReferenceCode(prefix) { + const possible = 'ABCDEFGHJKLMNPQRTUVWXYZ0123456789' + const length = 6 + let text = '' + + for (let i = 0; i < length; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)) + } + return prefix + text } module.exports = { diff --git a/app/services/return-logs/setup/period-used.service.js b/app/services/return-logs/setup/period-used.service.js new file mode 100644 index 0000000000..9707baea5d --- /dev/null +++ b/app/services/return-logs/setup/period-used.service.js @@ -0,0 +1,34 @@ +'use strict' + +/** + * Orchestrates fetching and presenting the data for `/return-logs/setup/{sessionId}/period-used` page + * @module PeriodUsedService + */ + +const PeriodUsedPresenter = require('../../../presenters/return-logs/setup/period-used.presenter.js') +const SessionModel = require('../../../models/session.model.js') + +/** + * Orchestrates fetching and presenting the data for `/return-logs/setup/{sessionId}/period-used` page + * + * Supports generating the data needed for the period used page in the return log setup journey. It fetches the + * current session record and formats the data needed for the page. + * + * @param {string} sessionId - The UUID of the current session + * + * @returns {Promise} The view data for the period used page + */ +async function go(sessionId) { + const session = await SessionModel.query().findById(sessionId) + + const formattedData = PeriodUsedPresenter.go(session) + + return { + activeNavBar: 'search', + ...formattedData + } +} + +module.exports = { + go +} diff --git a/app/services/return-logs/setup/submit-period-used.service.js b/app/services/return-logs/setup/submit-period-used.service.js new file mode 100644 index 0000000000..d464df6855 --- /dev/null +++ b/app/services/return-logs/setup/submit-period-used.service.js @@ -0,0 +1,110 @@ +'use strict' + +/** + * Orchestrates validating the data for `/return-logs/setup/{sessionId}/period-used` page + * @module SubmitPeriodUsedService + */ + +const PeriodUsedPresenter = require('../../../presenters/return-logs/setup/period-used.presenter.js') +const PeriodUsedValidator = require('../../../validators/return-logs/setup/period-used.validator.js') +const SessionModel = require('../../../models/session.model.js') + +/** + * Orchestrates validating the data for `/return-logs/setup/{sessionId}/period-used` page + * + * It first retrieves the session instance for the return log setup session in progress. The session has details about + * the return log that are needed to validate that the chosen date is valid. + * + * The validation result is then combined with the output of the presenter to generate the page data needed by the view. + * If there was a validation error the controller will re-render the page so needs this information. If all is well the + * controller will redirect to the next page in the journey. + * + * @param {string} sessionId - The UUID of the current session + * @param {object} payload - The submitted form data + * + * @returns {Promise} If no errors the page data for the period-used page else the validation error details + */ +async function go(sessionId, payload) { + const session = await SessionModel.query().findById(sessionId) + + const validationResult = _validate(payload, session) + + if (!validationResult) { + await _save(session, payload) + + return {} + } + + const formattedData = _submittedSessionData(session, payload) + + return { + activeNavBar: 'search', + error: validationResult, + ...formattedData + } +} + +async function _save(session, payload) { + session.periodDateUsedOptions = payload.periodDateUsedOptions + session.fromFullDate = payload.fromFullDate + session.toFullDate = payload.toFullDate + session.periodUsedFromDay = payload['period-used-from-day'] + session.periodUsedFromMonth = payload['period-used-from-month'] + session.periodUsedFromYear = payload['period-used-from-year'] + session.periodUsedToDay = payload['period-used-to-day'] + session.periodUsedToMonth = payload['period-used-to-month'] + session.periodUsedToYear = payload['period-used-to-year'] + + return session.$update() +} + +function _submittedSessionData(session, payload) { + session.periodDateUsedOptions = payload.periodDateUsedOptions ?? null + session.periodUsedFromDay = payload['period-used-from-day'] ?? null + session.periodUsedFromMonth = payload['period-used-from-month'] ?? null + session.periodUsedFromYear = payload['period-used-from-year'] ?? null + session.periodUsedToDay = payload['period-used-to-day'] ?? null + session.periodUsedToMonth = payload['period-used-to-month'] ?? null + session.periodUsedToYear = payload['period-used-to-year'] ?? null + + return PeriodUsedPresenter.go(session) +} + +function _validate(payload, session) { + const { startDate, endDate } = session + + const validation = PeriodUsedValidator.go(payload, startDate, endDate) + + if (!validation.error) { + return null + } + + const result = { + errorList: [] + } + + validation.error.details.forEach((detail) => { + let href + + if (detail.context.key === 'fromFullDate') { + href = '#from-full-date' + } else if (detail.context.key === 'toFullDate') { + href = '#to-full-date' + } else { + href = '#period-date-used-options' + } + + result.errorList.push({ + href, + text: detail.message + }) + + result[detail.context.key] = { message: detail.message } + }) + + return result +} + +module.exports = { + go +} diff --git a/app/services/return-logs/submit-view-return-log.service.js b/app/services/return-logs/submit-view-return-log.service.js new file mode 100644 index 0000000000..a0af5fd899 --- /dev/null +++ b/app/services/return-logs/submit-view-return-log.service.js @@ -0,0 +1,37 @@ +'use strict' + +/** + * Handles updating a return log record when the query button is clicked + * @module SubmitViewReturnLogService + */ + +const ReturnLogModel = require('../../models/return-log.model.js') + +/** + * Handles updating a return log record when the mark query button is clicked + * + * The mark query button in the view return log screen toggles whether or not a licence is 'under query'. + * + * If the return log is marked as under query then we update the `ReturnLogModel` record and set a `flash()` message in + * the session so that when the request is redirected to the `GET` it knows to display a notification banner to confirm. + * + * If the return log is marked as not under query (ie. the query is resolved) then we update the `ReturnLogModel` record + * but don't set a `flash()` message in the session as this is not part of the design. + * + * @param {string} returnLogId - The id of the return log to update + * @param {object} yar - The Hapi Yar session manager + * @param {object} payload - The submitted form data + */ +async function go(returnLogId, yar, payload) { + const markUnderQuery = payload['mark-query'] === 'mark' + + if (markUnderQuery) { + yar.flash('banner', 'This return has been marked under query.') + } + + await ReturnLogModel.query().findById(returnLogId).patch({ underQuery: markUnderQuery }) +} + +module.exports = { + go +} diff --git a/app/services/return-logs/view-return-log.service.js b/app/services/return-logs/view-return-log.service.js index d9a5aebac6..e258d6ef4a 100644 --- a/app/services/return-logs/view-return-log.service.js +++ b/app/services/return-logs/view-return-log.service.js @@ -14,16 +14,20 @@ const ViewReturnLogPresenter = require('../../presenters/return-logs/view-return * @param {string} returnId - The ID of the return log to view * @param {number} version - The version number of the associated return submission to view (0 means 'current') * @param {object} auth - The auth object taken from `request.auth` containing user details + * @param {object} yar - The Hapi `request.yar` session manager passed on by the controller * * @returns {Promise} an object representing the `pageData` needed by the view return log template. */ -async function go(returnId, version, auth) { +async function go(returnId, version, auth, yar) { const returnLog = await FetchReturnLogService.go(returnId, version) + const [notificationBannerMessage] = yar.flash('banner') + const pageData = ViewReturnLogPresenter.go(returnLog, auth) return { activeNavBar: 'search', + notificationBannerMessage, ...pageData } } diff --git a/app/validators/return-logs/setup/period-used.validator.js b/app/validators/return-logs/setup/period-used.validator.js new file mode 100644 index 0000000000..d94e4b623c --- /dev/null +++ b/app/validators/return-logs/setup/period-used.validator.js @@ -0,0 +1,99 @@ +'use strict' + +/** + * Validates data submitted for the `/return-logs/setup/{sessionId}/period-used` page + * @module PeriodUsedValidator + */ + +const Joi = require('joi').extend(require('@joi/date')) + +const { leftPadZeroes } = require('../../../presenters/base.presenter.js') + +/** + * Validates data submitted for the `/return-logs/setup/{sessionId}/period-used` page + * + * @param {object} payload - The payload from the request to be validated + * @param {Date} startDate - The start date of the period to validate the custom date entered by the user + * @param {Date} endDate - The end date of the period to validate the custom date entered by the user + * + * @returns {object} the result from calling Joi's schema.validate(). It will be an object with a `value:` property. If + * any errors are found the `error:` property will also exist detailing what the issues were + */ +function go(payload, startDate, endDate) { + const { + 'period-used-from-day': startDay, + 'period-used-from-month': startMonth, + 'period-used-from-year': startYear, + 'period-used-to-day': endDay, + 'period-used-to-month': endMonth, + 'period-used-to-year': endYear + } = payload + + payload.fromFullDate = _fullDate(startDay, startMonth, startYear) + payload.toFullDate = _fullDate(endDay, endMonth, endYear) + + return _validateDate(payload, startDate, endDate) +} + +/** + * A custom JOI validation function that checks that the fromFullDate is before or equal to the toFullDate. + * + * @param {object} value - the value to be validated + * @param {object} helpers - a Joi object containing a numbers of helpers + * + * @returns {object} If the fromFullDate is before or equal to the toFullDate, the value is returned. Else, a Joi error + * is returned. + */ +function _fromDateBeforeToDate(value, helpers) { + const { toFullDate, fromFullDate } = value + + if (fromFullDate <= toFullDate) { + return value + } + + return helpers.error('any.invalid') +} + +function _fullDate(day, month, year) { + const paddedMonth = month ? leftPadZeroes(month, 2) : '' + const paddedDay = day ? leftPadZeroes(day, 2) : '' + + return `${year}-${paddedMonth}-${paddedDay}` +} + +function _validateDate(payload, startDate, endDate) { + const schema = Joi.object({ + periodDateUsedOptions: Joi.string().required().messages({ + 'any.required': 'Select what period was used for this volume', + 'string.empty': 'Select what period was used for this volume' + }), + fromFullDate: Joi.alternatives().conditional('periodDateUsedOptions', { + is: 'custom-dates', + then: Joi.date().format(['YYYY-MM-DD']).min(startDate).required().messages({ + 'date.base': 'Enter a valid from date', + 'date.format': 'Enter a valid from date', + 'date.min': 'The from date must be within the return period start date' + }), + otherwise: Joi.optional() // Ensures this field is ignored if not using 'custom-dates' + }), + toFullDate: Joi.alternatives().conditional('periodDateUsedOptions', { + is: 'custom-dates', + then: Joi.date().format(['YYYY-MM-DD']).max(endDate).required().messages({ + 'date.base': 'Enter a valid to date', + 'date.format': 'Enter a valid to date', + 'date.max': 'The to date must be within the return periods end date' + }), + otherwise: Joi.optional() // Ensures this field is ignored if not using 'custom-dates' + }) + }) + .custom(_fromDateBeforeToDate, 'From date before to date') + .messages({ + 'any.invalid': 'The from date must be before the to date' + }) + + return schema.validate(payload, { abortEarly: false, allowUnknown: true }) +} + +module.exports = { + go +} diff --git a/app/validators/return-logs/setup/single-volume.validator.js b/app/validators/return-logs/setup/single-volume.validator.js index c1b4aebf8c..10963b2682 100644 --- a/app/validators/return-logs/setup/single-volume.validator.js +++ b/app/validators/return-logs/setup/single-volume.validator.js @@ -30,12 +30,12 @@ function go(payload) { 'any.only': singleVolumeError, 'string.empty': singleVolumeError }), - singleVolumeQuantity: Joi.number().min(0).when('singleVolume', { is: 'yes', then: Joi.required() }).messages({ + singleVolumeQuantity: Joi.number().positive().when('singleVolume', { is: 'yes', then: Joi.required() }).messages({ 'any.required': singleVolumeQuantityError, 'number.base': singleVolumeQuantityError, - 'number.min': singleVolumeQuantityError, 'number.max': singleVolumeQuantityError, - 'number.unsafe': singleVolumeQuantityError + 'number.unsafe': singleVolumeQuantityError, + 'number.positive': singleVolumeQuantityError }) }) diff --git a/app/validators/return-versions/setup/abstraction-period.validator.js b/app/validators/return-versions/setup/abstraction-period.validator.js index e1744eca7f..100ea71175 100644 --- a/app/validators/return-versions/setup/abstraction-period.validator.js +++ b/app/validators/return-versions/setup/abstraction-period.validator.js @@ -51,11 +51,11 @@ function _parsePayload(startDay, startMonth, endDay, endMonth) { const parsePayload = { startDate: { entry: `${parsedStartDay}${parsedStartMonth}`, - fullDate: `2023-${parsedStartMonth}-${parsedStartDay}` + fullDate: `1970-${parsedStartMonth}-${parsedStartDay}` }, endDate: { entry: `${parsedEndDay}${parsedEndMonth}`, - fullDate: `2023-${parsedEndMonth}-${parsedEndDay}` + fullDate: `1970-${parsedEndMonth}-${parsedEndDay}` } } diff --git a/app/views/return-logs/setup/period-used.njk b/app/views/return-logs/setup/period-used.njk new file mode 100644 index 0000000000..5bd4637e9d --- /dev/null +++ b/app/views/return-logs/setup/period-used.njk @@ -0,0 +1,134 @@ +{% extends 'layout.njk' %} +{% from "govuk/components/back-link/macro.njk" import govukBackLink %} +{% from "govuk/components/button/macro.njk" import govukButton %} +{% from "govuk/components/error-message/macro.njk" import govukErrorMessage %} +{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %} +{% from "govuk/components/radios/macro.njk" import govukRadios %} +{% from 'govuk/components/date-input/macro.njk' import govukDateInput %} + +{% block breadcrumbs %} + {# Back link #} + {{ + govukBackLink({ + text: 'Back', + href: backLink + }) + }} +{% endblock %} + +{% block content %} + {# Error summary #} + {% if error %} + {{ govukErrorSummary({ + titleText: "There is a problem", + errorList: error.errorList + }) }} + {%endif%} + + {# Main heading #} +
+ Return reference {{ returnReference }} +

{{ pageTitle }}

+
+
+
+ + + {% set dateInputHTML %} + {# From #} + {{ govukDateInput({ + id: 'period-used-from', + namePrefix: 'period-used-from', + errorMessage: { + text: error.fromFullDate.message + } if error.fromFullDate, + fieldset: { + legend: { + text: "From", + classes: "govuk-fieldset__legend--xs govuk-!-font-weight-bold" + } + }, + items: [ + { + classes: 'govuk-input--width-2 ' + errorClass, + name: 'day', + value: periodUsedFromDay + }, + { + classes: 'govuk-input--width-2 ' + errorClass, + name: 'month', + value: periodUsedFromMonth + }, + { + classes: 'govuk-input--width-4 ' + errorClass, + name: 'year', + value: periodUsedFromYear + } + ] + }) }} + + {# To #} + {{ govukDateInput({ + id: 'period-used-To', + namePrefix: 'period-used-to', + errorMessage: { + text: error.toFullDate.message + } if error.toFullDate, + fieldset: { + legend: { + text: "To", + classes: "govuk-fieldset__legend--xs govuk-!-font-weight-bold" + } + }, + items: [ + { + classes: 'govuk-input--width-2 ' + errorClass, + name: 'day', + value: periodUsedToDay + }, + { + classes: 'govuk-input--width-2 ' + errorClass, + name: 'month', + value: periodUsedToMonth + }, + { + classes: 'govuk-input--width-4 ' + errorClass, + name: 'year', + value: periodUsedToYear + } + ] + }) }} + {% endset %} + + {{ govukRadios({ + name: "periodDateUsedOptions", + errorMessage: { + text: error.periodDateUsedOptions.message + } if error.periodDateUsedOptions, + fieldset: { + legend: { + classes: "govuk-fieldset__legend--l" + } + }, + items: [ + { + value: "default", + text: "Default abstraction period", + hint: { text: abstractionPeriod }, + checked: periodDateUsedOptions === 'default' + }, + { + value: "custom-dates", + text: "Custom dates", + checked: periodDateUsedOptions === 'custom-dates', + conditional: { + html: dateInputHTML + } + } + ] + }) }} + + {{ govukButton({ text: "Continue", preventDoubleClick: true }) }} +
+
+{% endblock %} diff --git a/app/views/return-logs/view.njk b/app/views/return-logs/view.njk index 02e3cbc2d2..a41c99c085 100644 --- a/app/views/return-logs/view.njk +++ b/app/views/return-logs/view.njk @@ -4,6 +4,7 @@ {% from "govuk/components/button/macro.njk" import govukButton %} {% from "govuk/components/details/macro.njk" import govukDetails %} {% from "govuk/components/inset-text/macro.njk" import govukInsetText %} +{% from "govuk/components/notification-banner/macro.njk" import govukNotificationBanner %} {% from "govuk/components/summary-list/macro.njk" import govukSummaryList %} {% from "govuk/components/table/macro.njk" import govukTable %} {% from "govuk/components/warning-text/macro.njk" import govukWarningText %} @@ -18,6 +19,13 @@ {% endblock %} {% block content %} + {# Notification banner #} + {% if notificationBannerMessage %} + {{ govukNotificationBanner({ + text: notificationBannerMessage + }) }} + {%endif%} + {# Version warning #} {% if latest == false %} {{ govukWarningText({ @@ -104,20 +112,47 @@ {% endif %} - {# Submit/Edit return button #} - {% if actionButton %}
- {{ +
+ + {% if actionButton %} + {# The button group div goes inside the if because we only need a group when there's two buttons #} +
+ {# Submit/Edit return button #} + {{ + govukButton({ + text: actionButton.text, + preventDoubleClick: true, + href: actionButton.href + }) + }} + {% endif %} + + {# Mark as under query / Resolve query button #} + {% if underQuery %} + {% set queryButtonText = 'Resolve query' %} + {% set queryButtonValue = 'resolve' %} + {% else %} + {% set queryButtonText = 'Mark as under query' %} + {% set queryButtonValue = 'mark' %} + {% endif %} + {{ govukButton({ - text: actionButton.text, - preventDoubleClick: true, - href: actionButton.href + text: queryButtonText, + classes: "govuk-button--secondary", + name: "mark-query", + value: queryButtonValue, + preventDoubleClick: true }) }} + + {% if actionButton %} +
+ {% endif %} +
- {% endif %} {# Selected return submission details #} {% if displayTable %} diff --git a/package-lock.json b/package-lock.json index c5b9ddd192..6166b89dc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -313,16 +313,16 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.740.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.740.0.tgz", - "integrity": "sha512-X9aQOFJC3TsYwQP3AGcNhfYcFehVEHRKCHtHYOIKv5t1ydSJxpN/v34OrMMKvG1jFWMNkSYiSCVB9ZVo9KUwVA==", + "version": "3.741.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.741.0.tgz", + "integrity": "sha512-sZvdbRZ+E9/GcOMUOkZvYvob95N6c9LdzDneXHFASA7OIaEOQxQT1Arimz7JpEhfq/h9K2/j7wNO4jh4x80bmA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.734.0", - "@aws-sdk/credential-provider-node": "3.738.0", + "@aws-sdk/credential-provider-node": "3.741.0", "@aws-sdk/middleware-bucket-endpoint": "3.734.0", "@aws-sdk/middleware-expect-continue": "3.734.0", "@aws-sdk/middleware-flexible-checksums": "3.735.0", @@ -765,9 +765,9 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.734.0.tgz", - "integrity": "sha512-HEyaM/hWI7dNmb4NhdlcDLcgJvrilk8G4DQX6qz0i4pBZGC2l4iffuqP8K6ZQjUfz5/6894PzeFuhTORAMd+cg==", + "version": "3.741.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.741.0.tgz", + "integrity": "sha512-/XvnVp6zZXsyUlP1FtmspcWnd+Z1u2WK0wwzTE/x277M0oIhAezCW79VmcY4jcDQbYH+qMbtnBexfwgFDARxQg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.734.0", @@ -801,14 +801,14 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.738.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.738.0.tgz", - "integrity": "sha512-3MuREsazwBxghKb2sQQHvie+uuK4dX4/ckFYiSoffzJQd0YHxaGxf8cr4NOSCQCUesWu8D3Y0SzlnHGboVSkpA==", + "version": "3.741.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.741.0.tgz", + "integrity": "sha512-iz/puK9CZZkZjrKXX2W+PaiewHtlcD7RKUIsw4YHFyb8lrOt7yTYpM6VjeI+T//1sozjymmAnnp1SST9TXApLQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/credential-provider-env": "3.734.0", "@aws-sdk/credential-provider-http": "3.734.0", - "@aws-sdk/credential-provider-ini": "3.734.0", + "@aws-sdk/credential-provider-ini": "3.741.0", "@aws-sdk/credential-provider-process": "3.734.0", "@aws-sdk/credential-provider-sso": "3.734.0", "@aws-sdk/credential-provider-web-identity": "3.734.0", @@ -9195,9 +9195,9 @@ "dev": true }, "node_modules/neostandard": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/neostandard/-/neostandard-0.12.0.tgz", - "integrity": "sha512-MvtiRhevDzE+oqQUxFvDsEmipzy3erNmnz5q5TG9M8xZ30n86rt4PxGP9jgocGIZr1105OgPZNlK2FQEtb2Vng==", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/neostandard/-/neostandard-0.12.1.tgz", + "integrity": "sha512-As/LDK+xx591BLb1rPRaPs+JfXFgyNx5BoBui1KBeF/J4s0mW8+NBohrYnMfgm1w1t7E/Y/tU34MjMiP6lns6A==", "dev": true, "license": "MIT", "dependencies": { @@ -10487,9 +10487,10 @@ "integrity": "sha512-j05vL56tR90rsYqm9ZD05v6K4HI7t4yMDEvvU0x4f+IADXM9Jx1x9mzatxOs5drJq6dGhugxDW99mcPvXVLl+Q==" }, "node_modules/sass": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz", - "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==", + "version": "1.84.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.84.0.tgz", + "integrity": "sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==", + "license": "MIT", "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -11731,15 +11732,15 @@ } }, "@aws-sdk/client-s3": { - "version": "3.740.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.740.0.tgz", - "integrity": "sha512-X9aQOFJC3TsYwQP3AGcNhfYcFehVEHRKCHtHYOIKv5t1ydSJxpN/v34OrMMKvG1jFWMNkSYiSCVB9ZVo9KUwVA==", + "version": "3.741.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.741.0.tgz", + "integrity": "sha512-sZvdbRZ+E9/GcOMUOkZvYvob95N6c9LdzDneXHFASA7OIaEOQxQT1Arimz7JpEhfq/h9K2/j7wNO4jh4x80bmA==", "requires": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.734.0", - "@aws-sdk/credential-provider-node": "3.738.0", + "@aws-sdk/credential-provider-node": "3.741.0", "@aws-sdk/middleware-bucket-endpoint": "3.734.0", "@aws-sdk/middleware-expect-continue": "3.734.0", "@aws-sdk/middleware-flexible-checksums": "3.735.0", @@ -12089,9 +12090,9 @@ } }, "@aws-sdk/credential-provider-ini": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.734.0.tgz", - "integrity": "sha512-HEyaM/hWI7dNmb4NhdlcDLcgJvrilk8G4DQX6qz0i4pBZGC2l4iffuqP8K6ZQjUfz5/6894PzeFuhTORAMd+cg==", + "version": "3.741.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.741.0.tgz", + "integrity": "sha512-/XvnVp6zZXsyUlP1FtmspcWnd+Z1u2WK0wwzTE/x277M0oIhAezCW79VmcY4jcDQbYH+qMbtnBexfwgFDARxQg==", "requires": { "@aws-sdk/core": "3.734.0", "@aws-sdk/credential-provider-env": "3.734.0", @@ -12119,13 +12120,13 @@ } }, "@aws-sdk/credential-provider-node": { - "version": "3.738.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.738.0.tgz", - "integrity": "sha512-3MuREsazwBxghKb2sQQHvie+uuK4dX4/ckFYiSoffzJQd0YHxaGxf8cr4NOSCQCUesWu8D3Y0SzlnHGboVSkpA==", + "version": "3.741.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.741.0.tgz", + "integrity": "sha512-iz/puK9CZZkZjrKXX2W+PaiewHtlcD7RKUIsw4YHFyb8lrOt7yTYpM6VjeI+T//1sozjymmAnnp1SST9TXApLQ==", "requires": { "@aws-sdk/credential-provider-env": "3.734.0", "@aws-sdk/credential-provider-http": "3.734.0", - "@aws-sdk/credential-provider-ini": "3.734.0", + "@aws-sdk/credential-provider-ini": "3.741.0", "@aws-sdk/credential-provider-process": "3.734.0", "@aws-sdk/credential-provider-sso": "3.734.0", "@aws-sdk/credential-provider-web-identity": "3.734.0", @@ -18125,9 +18126,9 @@ "dev": true }, "neostandard": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/neostandard/-/neostandard-0.12.0.tgz", - "integrity": "sha512-MvtiRhevDzE+oqQUxFvDsEmipzy3erNmnz5q5TG9M8xZ30n86rt4PxGP9jgocGIZr1105OgPZNlK2FQEtb2Vng==", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/neostandard/-/neostandard-0.12.1.tgz", + "integrity": "sha512-As/LDK+xx591BLb1rPRaPs+JfXFgyNx5BoBui1KBeF/J4s0mW8+NBohrYnMfgm1w1t7E/Y/tU34MjMiP6lns6A==", "dev": true, "requires": { "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", @@ -19036,9 +19037,9 @@ "integrity": "sha512-j05vL56tR90rsYqm9ZD05v6K4HI7t4yMDEvvU0x4f+IADXM9Jx1x9mzatxOs5drJq6dGhugxDW99mcPvXVLl+Q==" }, "sass": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz", - "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==", + "version": "1.84.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.84.0.tgz", + "integrity": "sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==", "requires": { "@parcel/watcher": "^2.4.1", "chokidar": "^4.0.0", diff --git a/test/controllers/notifications-setup.controller.test.js b/test/controllers/notifications-setup.controller.test.js index 2e378e27a6..0e4c0e6560 100644 --- a/test/controllers/notifications-setup.controller.test.js +++ b/test/controllers/notifications-setup.controller.test.js @@ -11,6 +11,7 @@ const { expect } = Code const { postRequestOptions } = require('../support/general.js') // Things we need to stub +const DownloadRecipientsService = require('../../app/services/notifications/setup/download-recipients.service.js') const InitiateSessionService = require('../../app/services/notifications/setup/initiate-session.service.js') const ReturnsPeriodService = require('../../app/services/notifications/setup/returns-period.service.js') const ReviewService = require('../../app/services/notifications/setup/review.service.js') @@ -73,6 +74,36 @@ describe('Notifications Setup controller', () => { }) }) + describe('notifications/setup/download', () => { + describe('GET', () => { + beforeEach(async () => { + getOptions = { + method: 'GET', + url: basePath + `/${session.id}/download`, + auth: { + strategy: 'session', + credentials: { scope: ['returns'] } + } + } + }) + describe('when a request is valid', () => { + beforeEach(async () => { + Sinon.stub(InitiateSessionService, 'go').resolves(session) + Sinon.stub(DownloadRecipientsService, 'go').returns({ data: 'test', type: 'type/csv', filename: 'test.csv' }) + }) + + it('returns the file successfully', async () => { + const response = await server.inject(getOptions) + + expect(response.statusCode).to.equal(200) + expect(response.headers['content-type']).to.equal('type/csv') + expect(response.headers['content-disposition']).to.equal('attachment; filename="test.csv"') + expect(response.payload).to.equal('test') + }) + }) + }) + }) + describe('notifications/setup/returns-period', () => { describe('GET', () => { beforeEach(async () => { @@ -183,7 +214,7 @@ function _viewReturnsPeriod() { function _viewReview() { return { - pageTitle: 'Review the mailing listr', + pageTitle: 'Review the mailing list', activeNavBar: 'manage' } } diff --git a/test/controllers/return-logs-setup.controller.test.js b/test/controllers/return-logs-setup.controller.test.js index a488c26d10..6a4bc39127 100644 --- a/test/controllers/return-logs-setup.controller.test.js +++ b/test/controllers/return-logs-setup.controller.test.js @@ -625,7 +625,7 @@ describe('Return Logs Setup controller', () => { const response = await server.inject(_postOptions(path, {})) expect(response.statusCode).to.equal(302) - expect(response.headers.location).to.equal(`/system/return-logs/setup/${sessionId}/check-answers`) + expect(response.headers.location).to.equal(`/system/return-logs/setup/${sessionId}/check`) }) }) }) diff --git a/test/presenters/notifications/setup/download-recipients.presenter.test.js b/test/presenters/notifications/setup/download-recipients.presenter.test.js new file mode 100644 index 0000000000..64a211e263 --- /dev/null +++ b/test/presenters/notifications/setup/download-recipients.presenter.test.js @@ -0,0 +1,284 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') + +const { describe, it, beforeEach } = (exports.lab = Lab.script()) +const { expect } = Code + +// Thing under test +const DownloadRecipientsPresenter = require('../../../../app/presenters/notifications/setup/download-recipients.presenter.js') + +describe('Notifications Setup - Download recipients presenter', () => { + let recipients + + beforeEach(() => { + recipients = _recipients() + }) + + describe('when provided with "recipients"', () => { + it('correctly formats the data to a csv string', () => { + const result = DownloadRecipientsPresenter.go([ + recipients.primaryUser, + recipients.licenceHolder, + recipients.returnsTo, + recipients.organisation + ]) + + expect(result).to.equal( + // Headers + 'Licences,Return references,Returns period start date,Returns period end date,Returns due date,Message type,Message reference,Email,Recipient name,Address line 1,Address line 2,Address line 3,Address line 4,Address line 5,Address line 6,Postcode\n' + + // Row - Primary user + '"123/46","2434","2018-01-01","2019-01-01","2021-01-01","email","invitations","primary.user@important.com",,,,,,,,\n' + + // Row - Licence holder + '"1/343/3","376439279","2018-01-01","2019-01-01","2021-01-01","letter","invitations",,"Mr J Licence holder only","4","Privet Drive","Line 3","Line 4","Little Whinging","United Kingdom","WD25 7LR"\n' + + // Row - Returns to + '"1/343/3","376439279","2018-01-01","2019-01-01","2021-01-01","letter","invitations",,"Mr J Returns to (same licence ref as licence holder)","4","Privet Drive","Line 3","Line 4","Surrey","United Kingdom","WD25 7LR"\n' + + // Row - Licence holder - organisation + '"1/343/3","376439279","2018-01-01","2019-01-01","2021-01-01","letter","invitations",,"Gringotts","4","Privet Drive","Line 3","Line 4","Little Whinging","United Kingdom","WD25 7LR"\n' + ) + }) + + it('correctly formats the headers', () => { + const result = DownloadRecipientsPresenter.go([recipients.primaryUser]) + + let [headers] = result.split('\n') + // We want to test the header includes the new line + headers += '\n' + + expect(headers).to.equal( + 'Licences,' + + 'Return references,' + + 'Returns period start date,' + + 'Returns period end date,' + + 'Returns due date,' + + 'Message type,' + + 'Message reference,' + + 'Email,' + + 'Recipient name,' + + 'Address line 1,' + + 'Address line 2,' + + 'Address line 3,' + + 'Address line 4,' + + 'Address line 5,' + + 'Address line 6,' + + 'Postcode' + + '\n' + ) + }) + + describe('when the recipient is a "primary_user"', () => { + it('correctly formats the row', () => { + const result = DownloadRecipientsPresenter.go([recipients.primaryUser]) + + let [, row] = result.split('\n') + // We want to test the row includes the new line + row += '\n' + + expect(row).to.equal( + '"123/46",' + // Licences + '"2434",' + // 'Return references' + '"2018-01-01",' + // 'Returns period start date' + '"2019-01-01",' + // 'Returns period end date' + '"2021-01-01",' + // 'Returns due date' + '"email",' + // 'Message type' + '"invitations",' + // 'Message reference' + '"primary.user@important.com",' + // Email + ',' + // 'Recipient name'' + ',' + // 'Address line 1' + ',' + // 'Address line 2' + ',' + // 'Address line 3' + ',' + // 'Address line 4' + ',' + // 'Address line 5' + ',' + // 'Address line 6' + '\n' // Postcode and End of CSV line + ) + }) + }) + + describe('when the recipient has a "contact"', () => { + describe('and the "contact" is a "person"', () => { + describe('and the "person" is a "Licence holder"', () => { + it('correctly formats the row', () => { + const result = DownloadRecipientsPresenter.go([recipients.licenceHolder]) + + let [, row] = result.split('\n') + // We want to test the row includes the new line + row += '\n' + + expect(row).to.equal( + '"1/343/3",' + // Licences + '"376439279",' + // 'Return references' + '"2018-01-01",' + // 'Returns period start date' + '"2019-01-01",' + // 'Returns period end date' + '"2021-01-01",' + // 'Returns due date' + '"letter",' + // 'Message type' + '"invitations",' + // 'Message reference' + ',' + // Email + '"Mr J Licence holder only",' + // 'Recipient name'' + '"4",' + // 'Address line 1' + '"Privet Drive",' + // 'Address line 2' + '"Line 3",' + // 'Address line 3' + '"Line 4",' + // 'Address line 4' + '"Little Whinging",' + // 'Address line 5' - town + '"United Kingdom",' + // 'Address line 6' - country + '"WD25 7LR"' + // Postcode + '\n' // End of CSV line + ) + }) + }) + + describe('and the "person" is a "Returns to"', () => { + it('correctly formats the row', () => { + const result = DownloadRecipientsPresenter.go([recipients.returnsTo]) + + let [, row] = result.split('\n') + // We want to test the row includes the new line + row += '\n' + + expect(row).to.equal( + '"1/343/3",' + // Licences + '"376439279",' + // 'Return references' + '"2018-01-01",' + // 'Returns period start date' + '"2019-01-01",' + // 'Returns period end date' + '"2021-01-01",' + // 'Returns due date' + '"letter",' + // 'Message type' + '"invitations",' + // 'Message reference' + ',' + // Email + '"Mr J Returns to (same licence ref as licence holder)",' + // 'Recipient name'' + '"4",' + // 'Address line 1' + '"Privet Drive",' + // 'Address line 2' + '"Line 3",' + // 'Address line 3' + '"Line 4",' + // 'Address line 4' + '"Surrey",' + // 'Address line 5' - town / county + '"United Kingdom",' + // 'Address line 6' - country + '"WD25 7LR"' + // Postcode + '\n' // End of CSV line + ) + }) + }) + }) + + describe('and the "contact" is a "organisation"', () => { + it('correctly formats the row', () => { + const result = DownloadRecipientsPresenter.go([recipients.organisation]) + + let [, row] = result.split('\n') + // We want to test the row includes the new line + row += '\n' + + expect(row).to.equal( + '"1/343/3",' + // Licences + '"376439279",' + // 'Return references' + '"2018-01-01",' + // 'Returns period start date' + '"2019-01-01",' + // 'Returns period end date' + '"2021-01-01",' + // 'Returns due date' + '"letter",' + // 'Message type' + '"invitations",' + // 'Message reference' + ',' + // Email + '"Gringotts",' + // 'Recipient name'' - organisation + '"4",' + // 'Address line 1' + '"Privet Drive",' + // 'Address line 2' + '"Line 3",' + // 'Address line 3' + '"Line 4",' + // 'Address line 4' + '"Little Whinging",' + // 'Address line 5' - town + '"United Kingdom",' + // 'Address line 6' - country + '"WD25 7LR"' + // Postcode + '\n' // End of CSV line + ) + }) + }) + }) + }) +}) + +function _recipients() { + return { + primaryUser: { + contact: null, + contact_type: 'Primary user', + due_date: new Date('2021-01-01'), + email: 'primary.user@important.com', + end_date: new Date('2019-01-01'), + licence_ref: '123/46', + return_reference: '2434', + start_date: new Date('2018-01-01') + }, + licenceHolder: { + contact: { + addressLine1: '4', + addressLine2: 'Privet Drive', + addressLine3: 'Line 3', + addressLine4: 'Line 4', + country: 'United Kingdom', + county: 'Surrey', + forename: 'Harry', + initials: 'J', + name: 'Licence holder only', + postcode: 'WD25 7LR', + role: 'Licence holder', + salutation: 'Mr', + town: 'Little Whinging', + type: 'Person' + }, + contact_type: 'Licence holder', + due_date: new Date('2021-01-01'), + email: null, + end_date: new Date('2019-01-01'), + licence_ref: '1/343/3', + return_reference: '376439279', + start_date: new Date('2018-01-01') + }, + returnsTo: { + contact: { + addressLine1: '4', + addressLine2: 'Privet Drive', + addressLine3: 'Line 3', + addressLine4: 'Line 4', + country: 'United Kingdom', + county: 'Surrey', + forename: 'Harry', + initials: 'J', + name: 'Returns to (same licence ref as licence holder)', + postcode: 'WD25 7LR', + role: 'Licence holder', + salutation: 'Mr', + town: '', + type: 'Person' + }, + contact_type: 'Returns to', + due_date: new Date('2021-01-01'), + email: null, + end_date: new Date('2019-01-01'), + licence_ref: '1/343/3', + return_reference: '376439279', + start_date: new Date('2018-01-01') + }, + organisation: { + contact: { + addressLine1: '4', + addressLine2: 'Privet Drive', + addressLine3: 'Line 3', + addressLine4: 'Line 4', + country: 'United Kingdom', + county: 'Surrey', + forename: 'Harry', + initials: 'J', + name: 'Gringotts', + postcode: 'WD25 7LR', + role: 'Licence holder', + salutation: 'Mr', + town: 'Little Whinging', + type: 'Organisation' + }, + contact_type: 'Licence holder', + due_date: new Date('2021-01-01'), + email: null, + end_date: new Date('2019-01-01'), + licence_ref: '1/343/3', + return_reference: '376439279', + start_date: new Date('2018-01-01') + } + } +} diff --git a/test/presenters/return-logs/setup/period-used.presenter.test.js b/test/presenters/return-logs/setup/period-used.presenter.test.js new file mode 100644 index 0000000000..a0702a2e8a --- /dev/null +++ b/test/presenters/return-logs/setup/period-used.presenter.test.js @@ -0,0 +1,109 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') + +const { describe, it, beforeEach } = (exports.lab = Lab.script()) +const { expect } = Code + +// Thing under test +const PeriodUsedPresenter = require('../../../../app/presenters/return-logs/setup/period-used.presenter.js') + +describe('Return Logs Setup - Period Used presenter', () => { + let session + + beforeEach(() => { + session = { + id: '61e07498-f309-4829-96a9-72084a54996d', + returnReference: '012345', + periodStartDay: '01', + periodStartMonth: '04', + periodEndDay: '31', + periodEndMonth: '03' + } + }) + + describe('when provided with a session', () => { + it('correctly presents the data', () => { + const result = PeriodUsedPresenter.go(session) + + expect(result).to.equal({ + abstractionPeriod: '1 April to 31 March', + backLink: '/system/return-logs/setup/61e07498-f309-4829-96a9-72084a54996d/single-volume', + pageTitle: 'What period was used for this volume?', + returnReference: '012345', + sessionId: '61e07498-f309-4829-96a9-72084a54996d', + periodDateUsedOptions: null, + periodUsedFromDay: null, + periodUsedFromMonth: null, + periodUsedFromYear: null, + periodUsedToDay: null, + periodUsedToMonth: null, + periodUsedToYear: null + }) + }) + }) + + describe('the "periodDateUsedOptions" property', () => { + describe('when the user has previously selected the default abstraction date as the period used', () => { + beforeEach(() => { + session.periodDateUsedOptions = 'default' + }) + + it('returns the "periodDateUsedOptions" property populated to re-select the option', () => { + const result = PeriodUsedPresenter.go(session) + + expect(result.periodDateUsedOptions).to.equal('default') + }) + }) + + describe('when the user has previously selected custom date as the period used', () => { + beforeEach(() => { + session.periodDateUsedOptions = 'custom-dates' + }) + + it('returns the "periodDateUsedOptions" property populated to re-select the option', () => { + const result = PeriodUsedPresenter.go(session) + + expect(result.periodDateUsedOptions).to.equal('custom-dates') + }) + }) + }) + + describe('the "periodUsedFromDay", "periodUsedFromMonth" and "periodUsedFromYear" properties', () => { + describe('when the user has previously entered a period used from date', () => { + beforeEach(() => { + session.periodUsedFromDay = '1' + session.periodUsedFromMonth = '04' + session.periodUsedFromYear = '2023' + }) + + it('returns the "periodUsedFrom" properties populated to re-select the option', () => { + const result = PeriodUsedPresenter.go(session) + + expect(result.periodUsedFromDay).to.equal('1') + expect(result.periodUsedFromMonth).to.equal('04') + expect(result.periodUsedFromYear).to.equal('2023') + }) + }) + }) + + describe('the "periodUsedToDay", "periodUsedToMonth" and "periodUsedToYear" properties', () => { + describe('when the user has previously entered a period used to date', () => { + beforeEach(() => { + session.periodUsedToDay = '31' + session.periodUsedToMonth = '03' + session.periodUsedToYear = '2024' + }) + + it('returns the "periodUsedTo" properties populated to re-select the option', () => { + const result = PeriodUsedPresenter.go(session) + + expect(result.periodUsedToDay).to.equal('31') + expect(result.periodUsedToMonth).to.equal('03') + expect(result.periodUsedToYear).to.equal('2024') + }) + }) + }) +}) diff --git a/test/presenters/return-logs/view-return-log.presenter.test.js b/test/presenters/return-logs/view-return-log.presenter.test.js index 4c83d411b9..1f94b333dc 100644 --- a/test/presenters/return-logs/view-return-log.presenter.test.js +++ b/test/presenters/return-logs/view-return-log.presenter.test.js @@ -608,6 +608,36 @@ describe('View Return Log presenter', () => { }) }) }) + + describe('the "underQuery" property', () => { + beforeEach(() => { + setupSubmission(testReturnLog) + }) + + describe('when the return log is under query', () => { + beforeEach(() => { + testReturnLog.underQuery = true + }) + + it('returns true', () => { + const result = ViewReturnLogPresenter.go(testReturnLog, auth) + + expect(result.underQuery).to.equal(true) + }) + }) + + describe('when the return log is not under query', () => { + beforeEach(() => { + testReturnLog.underQuery = false + }) + + it('returns false', () => { + const result = ViewReturnLogPresenter.go(testReturnLog, auth) + + expect(result.underQuery).to.equal(false) + }) + }) + }) }) function setupSubmission(testReturnLog, nilReturn = false) { diff --git a/test/services/notifications/setup/download-recipients.service.test.js b/test/services/notifications/setup/download-recipients.service.test.js new file mode 100644 index 0000000000..f06d281114 --- /dev/null +++ b/test/services/notifications/setup/download-recipients.service.test.js @@ -0,0 +1,84 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') +const Sinon = require('sinon') + +const { describe, it, before, after } = (exports.lab = Lab.script()) +const { expect } = Code + +// Test helpers +const FetchDownloadRecipientsService = require('../../../../app/services/notifications/setup/fetch-download-recipients.service.js') +const SessionHelper = require('../../../support/helpers/session.helper.js') + +// Thing under test +const DownloadRecipientsService = require('../../../../app/services/notifications/setup/download-recipients.service.js') + +describe('Notifications Setup - Download recipients service', () => { + const referenceCode = 'RINV-00R1MQ' + const year = 2025 + + let clock + let session + let testRecipients + + before(async () => { + clock = Sinon.useFakeTimers(new Date(`${year}-01-01`)) + + session = await SessionHelper.add({ + data: { returnsPeriod: 'quarterFour', referenceCode, notificationType: 'Returns invitation' } + }) + + testRecipients = _recipients() + Sinon.stub(FetchDownloadRecipientsService, 'go').resolves(testRecipients) + }) + + after(() => { + clock.restore() + }) + + it('correctly returns the csv string, filename and type', async () => { + const result = await DownloadRecipientsService.go(session.id) + + expect(result).to.equal({ + data: + // Headers + 'Licences,Return references,Returns period start date,Returns period end date,Returns due date,Message type,Message reference,Email,Recipient name,Address line 1,Address line 2,Address line 3,Address line 4,Address line 5,Address line 6,Postcode\n' + + // Row - licence holder + '"1/343/3","376439279","2018-01-01","2019-01-01","2021-01-01","letter","invitations",,"Mr J Licence holder only","4","Privet Drive","Line 3","Line 4","Little Whinging","United Kingdom","WD25 7LR"\n', + filename: `Returns invitation - ${referenceCode}.csv`, + type: 'text/csv' + }) + }) +}) + +function _recipients() { + return [ + { + contact: { + addressLine1: '4', + addressLine2: 'Privet Drive', + addressLine3: 'Line 3', + addressLine4: 'Line 4', + country: 'United Kingdom', + county: 'Surrey', + forename: 'Harry', + initials: 'J', + name: 'Licence holder only', + postcode: 'WD25 7LR', + role: 'Licence holder', + salutation: 'Mr', + town: 'Little Whinging', + type: 'Person' + }, + contact_type: 'Licence holder', + due_date: new Date('2021-01-01'), + email: null, + end_date: new Date('2019-01-01'), + licence_ref: '1/343/3', + return_reference: '376439279', + start_date: new Date('2018-01-01') + } + ] +} diff --git a/test/services/notifications/setup/initiate-session.service.test.js b/test/services/notifications/setup/initiate-session.service.test.js index dce38fb4bf..17d8441c1f 100644 --- a/test/services/notifications/setup/initiate-session.service.test.js +++ b/test/services/notifications/setup/initiate-session.service.test.js @@ -15,12 +15,26 @@ const InitiateSessionService = require('../../../../app/services/notifications/s describe('Notifications Setup - Initiate Session service', () => { describe('when called', () => { - it('creates a new session record with an empty data property', async () => { + it('creates a new session record', async () => { const result = await InitiateSessionService.go() const matchingSession = await SessionModel.query().findById(result.id) - expect(matchingSession.data).to.equal({}) + expect(matchingSession.data).to.equal({ + notificationType: 'Returns invitation', + referenceCode: matchingSession.referenceCode // randomly generated + }) + }) + + describe('the "referenceCode" property', () => { + it('returns a reference code for an "invitation" notification', async () => { + const result = await InitiateSessionService.go() + + const matchingSession = await SessionModel.query().findById(result.id) + + expect(matchingSession.referenceCode).to.include('RINV-') + expect(matchingSession.referenceCode.length).to.equal(11) + }) }) }) }) diff --git a/test/services/return-logs/setup/period-used.service.test.js b/test/services/return-logs/setup/period-used.service.test.js new file mode 100644 index 0000000000..83905b34d5 --- /dev/null +++ b/test/services/return-logs/setup/period-used.service.test.js @@ -0,0 +1,60 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') + +const { describe, it, before } = (exports.lab = Lab.script()) +const { expect } = Code + +// Test helpers +const SessionHelper = require('../../../support/helpers/session.helper.js') + +// Thing under test +const PeriodUsedService = require('../../../../app/services/return-logs/setup/period-used.service.js') + +describe('Return Logs Setup - Period used service', () => { + let session + + before(async () => { + session = await SessionHelper.add({ + data: { + returnReference: '012345', + periodStartDay: '01', + periodStartMonth: '04', + periodEndDay: '31', + periodEndMonth: '03' + } + }) + }) + + describe('when called', () => { + it('fetches the current setup session record', async () => { + const result = await PeriodUsedService.go(session.id) + + expect(result.sessionId).to.equal(session.id) + }) + + it('returns page data for the view', async () => { + const result = await PeriodUsedService.go(session.id) + + expect(result).to.equal( + { + abstractionPeriod: '1 April to 31 March', + activeNavBar: 'search', + backLink: `/system/return-logs/setup/${session.id}/single-volume`, + pageTitle: 'What period was used for this volume?', + returnReference: '012345', + periodDateUsedOptions: null, + periodUsedFromDay: null, + periodUsedFromMonth: null, + periodUsedFromYear: null, + periodUsedToDay: null, + periodUsedToMonth: null, + periodUsedToYear: null + }, + { skip: ['sessionId'] } + ) + }) + }) +}) diff --git a/test/services/return-logs/setup/submit-period-used.service.test.js b/test/services/return-logs/setup/submit-period-used.service.test.js new file mode 100644 index 0000000000..57c653d7a4 --- /dev/null +++ b/test/services/return-logs/setup/submit-period-used.service.test.js @@ -0,0 +1,126 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') + +const { describe, it, beforeEach } = (exports.lab = Lab.script()) +const { expect } = Code + +// Test helpers +const SessionHelper = require('../../../support/helpers/session.helper.js') + +// Thing under test +const SubmitPeriodUsedService = require('../../../../app/services/return-logs/setup/submit-period-used.service.js') + +describe('Return Logs Setup - Submit Period Used service', () => { + let payload + let session + let sessionData + + beforeEach(async () => { + sessionData = { + data: { + returnReference: '12345', + startDate: '2023-04-01', + endDate: '2024-03-31', + periodStartDay: '01', + periodStartMonth: '04', + periodEndDay: '31', + periodEndMonth: '03' + } + } + + session = await SessionHelper.add(sessionData) + }) + + describe('when called', () => { + describe('with a valid payload', () => { + describe('because the user entered the default abstraction period', () => { + beforeEach(async () => { + payload = { periodDateUsedOptions: 'default' } + }) + + it('saves the submitted option', async () => { + await SubmitPeriodUsedService.go(session.id, payload) + + const refreshedSession = await session.$query() + + expect(refreshedSession.periodDateUsedOptions).to.equal('default') + }) + }) + + describe('because the user entered a custom period', () => { + beforeEach(async () => { + payload = { + periodDateUsedOptions: 'custom-dates', + 'period-used-from-day': '01', + 'period-used-from-month': '04', + 'period-used-from-year': '2023', + 'period-used-to-day': '31', + 'period-used-to-month': '03', + 'period-used-to-year': '2024' + } + }) + + it('saves the submitted option', async () => { + await SubmitPeriodUsedService.go(session.id, payload) + + const refreshedSession = await session.$query() + + expect(refreshedSession.periodDateUsedOptions).to.equal('custom-dates') + expect(refreshedSession.periodUsedFromDay).to.equal('01') + expect(refreshedSession.periodUsedFromMonth).to.equal('04') + expect(refreshedSession.periodUsedFromYear).to.equal('2023') + expect(refreshedSession.periodUsedToDay).to.equal('31') + expect(refreshedSession.periodUsedToMonth).to.equal('03') + expect(refreshedSession.periodUsedToYear).to.equal('2024') + }) + }) + }) + + describe('with an invalid payload', () => { + beforeEach(async () => { + payload = {} + }) + + it('returns the page data for the view', async () => { + const result = await SubmitPeriodUsedService.go(session.id, payload) + + expect(result).to.equal( + { + abstractionPeriod: '1 April to 31 March', + activeNavBar: 'search', + backLink: `/system/return-logs/setup/${session.id}/single-volume`, + pageTitle: 'What period was used for this volume?', + returnReference: '12345', + periodDateUsedOptions: null, + periodUsedFromDay: null, + periodUsedFromMonth: null, + periodUsedFromYear: null, + periodUsedToDay: null, + periodUsedToMonth: null, + periodUsedToYear: null + }, + { skip: ['sessionId', 'error'] } + ) + }) + + describe('because the user has not selected anything', () => { + it('includes an error for the radio form element', async () => { + const result = await SubmitPeriodUsedService.go(session.id, payload) + + expect(result.error).to.equal({ + errorList: [ + { + href: '#period-date-used-options', + text: 'Select what period was used for this volume' + } + ], + periodDateUsedOptions: { message: 'Select what period was used for this volume' } + }) + }) + }) + }) + }) +}) diff --git a/test/services/return-logs/submit-view-return-log.service.test.js b/test/services/return-logs/submit-view-return-log.service.test.js new file mode 100644 index 0000000000..b342dac0ed --- /dev/null +++ b/test/services/return-logs/submit-view-return-log.service.test.js @@ -0,0 +1,82 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') +const Sinon = require('sinon') + +const { describe, it, beforeEach, afterEach } = (exports.lab = Lab.script()) +const { expect } = Code + +// Test helpers +const ReturnLogHelper = require('../../support/helpers/return-log.helper.js') +const ReturnLogModel = require('../../../app/models/return-log.model.js') + +// Thing under test +const SubmitViewReturnLogService = require('../../../app/services/return-logs/submit-view-return-log.service.js') + +describe('Submit View Return Log Service', () => { + let payload + let patchStub + let mockReturnLog + let yarStub + + beforeEach(async () => { + mockReturnLog = ReturnLogModel.fromJson({ ...ReturnLogHelper.defaults() }) + + patchStub = Sinon.stub().resolves() + Sinon.stub(ReturnLogModel, 'query').returns({ + findById: Sinon.stub().withArgs(mockReturnLog.id).returnsThis(), + patch: patchStub + }) + + yarStub = { flash: Sinon.stub() } + }) + + afterEach(() => { + Sinon.restore() + }) + + describe('when called', () => { + describe('and the user is marking the return log as under query', () => { + beforeEach(() => { + payload = { 'mark-query': 'mark' } + }) + + it('sets a flash message and updates the status of the return log', async () => { + await SubmitViewReturnLogService.go(mockReturnLog.id, yarStub, payload) + + // Check we save the status change + const [patchObject] = patchStub.args[0] + + expect(patchObject).to.equal({ underQuery: true }) + + // Check we add the flash message + const [flashType, bannerMessage] = yarStub.flash.args[0] + + expect(flashType).to.equal('banner') + expect(bannerMessage).to.equal('This return has been marked under query.') + }) + }) + + describe('and the user is marking the return log query as resolved', () => { + beforeEach(() => { + payload = { 'mark-query': 'resolve' } + }) + + it('updates the status of the return log with no flash message set', async () => { + await SubmitViewReturnLogService.go(mockReturnLog.id, yarStub, payload) + + // Check we save the status change + const [patchObject] = patchStub.args[0] + + expect(patchObject).to.equal({ underQuery: false }) + + // Check there is no flash message + const flashArgs = yarStub.flash.args[0] + + expect(flashArgs).to.not.exist() + }) + }) + }) +}) diff --git a/test/services/return-logs/view-return-log.service.test.js b/test/services/return-logs/view-return-log.service.test.js index 55c8c75262..e5f806fe8c 100644 --- a/test/services/return-logs/view-return-log.service.test.js +++ b/test/services/return-logs/view-return-log.service.test.js @@ -21,6 +21,8 @@ const ReturnLogHelper = require('../../support/helpers/return-log.helper.js') const ViewReturnLogService = require('../../../app/services/return-logs/view-return-log.service.js') describe('View Return Log service', () => { + let yarStub + beforeEach(() => { const mockReturnLog = ReturnLogModel.fromJson({ ...ReturnLogHelper.defaults({ @@ -30,6 +32,10 @@ describe('View Return Log service', () => { }) Sinon.stub(FetchReturnLogService, 'go').resolves(mockReturnLog) + + yarStub = { + flash: Sinon.stub().returns(['NOTIFICATION_BANNER_MESSAGE']) + } }) afterEach(() => { @@ -37,12 +43,13 @@ describe('View Return Log service', () => { }) it('correctly fetches return log and transforms it via the presenter', async () => { - const result = await ViewReturnLogService.go('RETURN_ID', 0, { credentials: { scope: ['returns'] } }) + const result = await ViewReturnLogService.go('RETURN_ID', 0, { credentials: { scope: ['returns'] } }, yarStub) // We only check a couple of items here -- the key thing is that the mock return log was fetched and successfully // passed to the presenter expect(result).to.include({ activeNavBar: 'search', + notificationBannerMessage: 'NOTIFICATION_BANNER_MESSAGE', pageTitle: 'Abstraction return' }) }) diff --git a/test/validators/return-logs/setup/period-used.validator.test.js b/test/validators/return-logs/setup/period-used.validator.test.js new file mode 100644 index 0000000000..4ed7def37b --- /dev/null +++ b/test/validators/return-logs/setup/period-used.validator.test.js @@ -0,0 +1,167 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') + +const { describe, it, beforeEach } = (exports.lab = Lab.script()) +const { expect } = Code + +// Thing under test +const PeriodUsedValidator = require('../../../../app/validators/return-logs/setup/period-used.validator.js') + +describe('Return Logs Setup - Period Used validator', () => { + const startDate = '2023-04-01' + const endDate = '2024-03-31' + + let payload + + describe('when a valid payload is provided', () => { + describe('because the user selected the "default" option', () => { + beforeEach(() => { + payload = { periodDateUsedOptions: 'default' } + }) + + it('confirms the payload is valid', () => { + const result = PeriodUsedValidator.go(payload, startDate, endDate) + + expect(result.error).not.to.exist() + }) + }) + + describe('because the user selected the "custom-dates" option', () => { + beforeEach(() => { + payload = { + periodDateUsedOptions: 'custom-dates', + 'period-used-from-day': '01', + 'period-used-from-month': '04', + 'period-used-from-year': '2023', + 'period-used-to-day': '31', + 'period-used-to-month': '03', + 'period-used-to-year': '2024' + } + }) + + it('confirms the payload is valid', () => { + const result = PeriodUsedValidator.go(payload, startDate, endDate) + + expect(result.error).not.to.exist() + }) + }) + }) + + describe('when an invalid payload is provided', () => { + describe('because the user did not select an option', () => { + beforeEach(() => { + payload = {} + }) + + it('fails validation with the message "Select what period was used for this volume"', () => { + const result = PeriodUsedValidator.go(payload, startDate, endDate) + + expect(result.error).to.exist() + expect(result.error.details[0].message).to.equal('Select what period was used for this volume') + }) + }) + + describe('because the user selected "custom-dates" but did not enter anything', () => { + beforeEach(() => { + payload = { periodDateUsedOptions: 'custom-dates' } + }) + + it('fails validation with the message "Enter a valid from date" and "Enter a valid to date"', () => { + const result = PeriodUsedValidator.go(payload, startDate, endDate) + + expect(result.error).to.exist() + expect(result.error.details[0].message).to.equal('Enter a valid from date') + expect(result.error.details[1].message).to.equal('Enter a valid to date') + }) + }) + + describe('because the user selected "custom-dates" and entered an invalid date', () => { + beforeEach(() => { + payload = { + periodDateUsedOptions: 'custom-dates', + 'period-used-from-day': '99', + 'period-used-from-month': '99', + 'period-used-from-year': '9999', + 'period-used-to-day': '00', + 'period-used-to-month': '00', + 'period-used-to-year': '0000' + } + }) + + it('fails validation with the message "Enter a valid from date" and "Enter a valid to date"', () => { + const result = PeriodUsedValidator.go(payload, startDate, endDate) + + expect(result.error).to.exist() + expect(result.error.details[0].message).to.equal('Enter a valid from date') + expect(result.error.details[1].message).to.equal('Enter a valid to date') + }) + }) + + describe('because the user selected "custom-dates" and entered a text', () => { + beforeEach(() => { + payload = { + periodDateUsedOptions: 'custom-dates', + 'period-used-from-day': 'hh', + 'period-used-from-month': 'ii', + 'period-used-from-year': 'abcd', + 'period-used-to-day': 'ab', + 'period-used-to-month': 'cd', + 'period-used-to-year': 'efgh' + } + }) + + it('fails validation with the message "Enter a valid from date" and "Enter a valid to date"', () => { + const result = PeriodUsedValidator.go(payload, startDate, endDate) + + expect(result.error).to.exist() + expect(result.error.details[0].message).to.equal('Enter a valid from date') + expect(result.error.details[1].message).to.equal('Enter a valid to date') + }) + }) + + describe('because the user selected "custom-dates" and entered a date outside the return period', () => { + beforeEach(() => { + payload = { + periodDateUsedOptions: 'custom-dates', + 'period-used-from-day': '01', + 'period-used-from-month': '04', + 'period-used-from-year': '2024', + 'period-used-to-day': '31', + 'period-used-to-month': '03', + 'period-used-to-year': '2025' + } + }) + + it('fails validation with the message "The to date must be within the return periods end date"', () => { + const result = PeriodUsedValidator.go(payload, startDate, endDate) + + expect(result.error).to.exist() + expect(result.error.details[0].message).to.equal('The to date must be within the return periods end date') + }) + }) + + describe('because the user selected "custom-dates" and entered the to and from date the wrong way round', () => { + beforeEach(() => { + payload = { + periodDateUsedOptions: 'custom-dates', + 'period-used-from-day': '31', + 'period-used-from-month': '03', + 'period-used-from-year': '2024', + 'period-used-to-day': '01', + 'period-used-to-month': '03', + 'period-used-to-year': '2023' + } + }) + + it('fails validation with the message "Enter a valid from date" and "Enter a valid to date"', () => { + const result = PeriodUsedValidator.go(payload, startDate, endDate) + + expect(result.error).to.exist() + expect(result.error.details[0].message).to.equal('The from date must be before the to date') + }) + }) + }) +}) diff --git a/test/validators/return-logs/setup/single-volume.validator.test.js b/test/validators/return-logs/setup/single-volume.validator.test.js index d103c81342..5d4ca8e152 100644 --- a/test/validators/return-logs/setup/single-volume.validator.test.js +++ b/test/validators/return-logs/setup/single-volume.validator.test.js @@ -80,7 +80,7 @@ describe('Return Logs Setup - Single Volume validator', () => { }) }) - describe('but entered a volume too small', () => { + describe('but entered a negative volume', () => { beforeEach(() => { payload.singeVolumeQuantity = '-0.1' }) @@ -93,6 +93,19 @@ describe('Return Logs Setup - Single Volume validator', () => { }) }) + describe('but entered a volume too small', () => { + beforeEach(() => { + payload.singeVolumeQuantity = '0' + }) + + it('fails validation with the message "Enter a total figure"', () => { + const result = SingleVolumeValidator.go(payload) + + expect(result.error).to.exist() + expect(result.error.details[0].message).to.equal('Enter a total figure') + }) + }) + describe('but entered a volume too big', () => { beforeEach(() => { payload.singeVolumeQuantity = '9007199254740992'