Skip to content
Merged
34,879 changes: 23,598 additions & 11,281 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"@aws-sdk/client-s3": "^3.679.0",
"@aws-sdk/client-sqs": "^3.982.0",
"@aws-sdk/s3-request-presigner": "^3.679.0",
"@defra/forms-engine-plugin": "^4.0.57",
"@defra/forms-engine-plugin": "^4.4.0",
"@defra/forms-model": "^3.0.638",
"@defra/hapi-tracing": "^1.12.0",
"@elastic/ecs-pino-format": "^1.5.0",
Expand Down
1 change: 1 addition & 0 deletions src/api/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
*/

/**
* @typedef {{ Params: { referenceNumber: string }}} GetSubmissionByReference
* @typedef {{ Params: { dlq: string }}} DeadLetterQueueRequest
*/

Expand Down
17 changes: 17 additions & 0 deletions src/models/form.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { formAdapterSubmissionMessagePayloadSchema } from '@defra/forms-engine-plugin/engine/types/schema.js'
import Joi from 'joi'

export const magicLinkSchema = Joi.string().uuid().required()
Expand Down Expand Up @@ -38,6 +39,7 @@ export const validateSavedLinkResponseSchema = Joi.object({
export const generateFormSubmissionsFileResponseSchema = Joi.object({
message: Joi.string().required()
}).label('generateFormSubmissionsFileResponse')

export const generateFeedbackSubmissionsFileResponseSchema = Joi.object({
message: Joi.string().required()
}).label('generateFeedbackSubmissionsFileResponse')
Expand All @@ -47,6 +49,21 @@ export const resetSaveAndExitLinkResponseSchema = Joi.object({
recordUpdated: Joi.boolean().required()
}).label('resetSaveAndExitLinkResponseSchema')

/**
* @type {Joi.ObjectSchema<FormSubmissionDocument>}
*/
export const getSubmissionByReferenceResponseSchema = Joi.object()
.keys({
_id: Joi.string().hex().required(),
recordCreatedAt: Joi.string().isoDate().required(),
expireAt: Joi.string().isoDate().required()
})
.concat(formAdapterSubmissionMessagePayloadSchema)
.label('getSubmissionByReferenceResponseSchema')

/**
* @import { FormSubmissionDocument } from '~/src/api/types.js'
*/
export const dqlSchema = Joi.string().valid('form-submissions', 'save-and-exit')

export const receiptHandleSchema = Joi.string()
1 change: 1 addition & 0 deletions src/repositories/__stubs__/submission.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FormStatus } from '@defra/forms-model'
import { addDays } from '~/src/helpers/date-helper.js'

export const STUB_FORM_ID = '688131eeff67f889d52c66cc'
export const STUB_SUBMISSION_REF = '365-DFR-C67'
export const STUB_SUBMISSION_RECORD_ID = '68d284ef5fa1a0fb2ede066a'

/**
Expand Down
29 changes: 29 additions & 0 deletions src/repositories/submission-repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,35 @@ export async function createSubmissionRecord(document, session) {
}
}

/**
* Gets a submission record based on reference number
* @param {string} referenceNumber - the reference number
* @returns { Promise<WithId<FormSubmissionDocument> | null> }
*/
export async function getSubmissionRecordByReference(referenceNumber) {
logger.info('Reading submission record')

const coll = /** @type {Collection<FormSubmissionDocument>} */ (
db.collection(SUBMISSIONS_COLLECTION_NAME)
)

try {
const result = await coll.findOne({
'meta.referenceNumber': referenceNumber
})

logger.info('Read submission record')

return result
} catch (err) {
logger.error(
err,
`Failed to read submission record - ${getErrorMessage(err)}`
)
throw err
}
}

/**
* @import { ClientSession, ObjectId, WithId, Collection, FindCursor } from 'mongodb'
* @import { FormSubmissionDocument } from '~/src/api/types.js'
Expand Down
23 changes: 23 additions & 0 deletions src/repositories/submission-repository.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { db } from '~/src/mongo.js'
import { buildMockCollection } from '~/src/repositories/__stubs__/mongo.js'
import {
STUB_FORM_ID,
STUB_SUBMISSION_REF,
buildDbDocument
} from '~/src/repositories/__stubs__/submission.js'
import {
createSubmissionRecord,
getSubmissionRecordByReference,
getSubmissionRecords
} from '~/src/repositories/submission-repository.js'

Expand Down Expand Up @@ -103,4 +105,25 @@ describe('submission repository', () => {
).rejects.toThrow(new Error('Failed'))
})
})

describe('getSubmissionRecordByReference', () => {
it('should get submission record', async () => {
jest.mocked(
mockCollection.findOne.mockResolvedValueOnce(submissionDocument)
)
const submissionRecord =
await getSubmissionRecordByReference(STUB_SUBMISSION_REF)
expect(submissionRecord).toEqual(submissionDocument)
})

it('should handle get submission record failures', async () => {
mockCollection.findOne.mockImplementation(() => {
throw new Error('an error')
})

await expect(
getSubmissionRecordByReference(STUB_SUBMISSION_REF)
).rejects.toThrow(new Error('an error'))
})
})
})
2 changes: 1 addition & 1 deletion src/routes/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,5 +218,5 @@ export default [

/**
* @import { ServerRoute } from '@hapi/hapi'
* @import { DeadLetterQueueRequest, DeadLetterQueueAndHandleRequest, GenerateFeedbackSubmissionsFile, GenerateFormSubmissionsFile, ResetSaveAndExit } from '~/src/api/types.js'
* @import { DeadLetterQueueRequest, DeadLetterQueueAndHandleRequest, GenerateFeedbackSubmissionsFile, GenerateFormSubmissionsFile, GetSubmissionByReference, ResetSaveAndExit } from '~/src/api/types.js'
*/
3 changes: 2 additions & 1 deletion src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ import admin from '~/src/routes/admin.js'
import files from '~/src/routes/files.js'
import form from '~/src/routes/form.js'
import health from '~/src/routes/health.js'
import submission from '~/src/routes/submission.js'

export default [health, files, form, admin].flat()
export default [health, files, form, submission, admin].flat()
53 changes: 53 additions & 0 deletions src/routes/submission.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Scopes } from '@defra/forms-model'
import Boom from '@hapi/boom'
import Joi from 'joi'

import { getSubmissionByReferenceResponseSchema } from '~/src/models/form.js'
import { getSubmissionRecordByReference } from '~/src/repositories/submission-repository.js'

export default [
/**
* @satisfies {ServerRoute<GetSubmissionByReference>}
*/
({
method: 'GET',
path: '/submission/{referenceNumber}',
async handler(request) {
const { params } = request
const { referenceNumber } = params

const record = await getSubmissionRecordByReference(referenceNumber)

if (!record) {
return Boom.notFound(
`Submission record with reference ${referenceNumber} was not found`
)
}

return record
},
options: {
tags: ['api'],
auth: {
scope: [`+${Scopes.FormRead}`]
},
validate: {
params: Joi.object()
.keys({
referenceNumber: Joi.string().required()
})
.label('getSubmissionByReferenceParams')
},
response: {
status: {
200: getSubmissionByReferenceResponseSchema
}
}
}
})
]

/**
* @import { ServerRoute } from '@hapi/hapi'
* @import { GetSubmissionByReference } from '~/src/api/types.js'
*/
59 changes: 59 additions & 0 deletions src/routes/submission.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { StatusCodes } from 'http-status-codes'

import { createServer } from '~/src/api/server.js'
import { STUB_SUBMISSION_REF } from '~/src/repositories/__stubs__/submission.js'
import { getSubmissionRecordByReference } from '~/src/repositories/submission-repository.js'
import { authAdmin } from '~/test/fixtures/auth.js'
// @ts-expect-error - import json
import formSubmissions from '~/test/fixtures/forms-submissions.json'

jest.mock('~/src/mongo.js')
jest.mock('~/src/repositories/submission-repository.js')

describe('Submission routes', () => {
/** @type {Server} */
let server

beforeAll(async () => {
server = await createServer()
await server.initialize()
})

afterAll(() => {
return server.stop()
})

describe('Get submission record', () => {
test('Testing GET /submission/{referenceNumber} route is successful with valid params', async () => {
const expectedRecord = formSubmissions.at(0)
jest
.mocked(getSubmissionRecordByReference)
// @ts-expect-error - test data is not fully compliant with FormSubmissionDocument type
.mockResolvedValue(expectedRecord)

const response = await server.inject({
method: 'GET',
url: `/submission/${STUB_SUBMISSION_REF}`,
auth: authAdmin
})

expect(response.statusCode).toEqual(StatusCodes.OK)
})

test('Testing GET /submission/{referenceNumber} returns 404 when record not found', async () => {
jest.mocked(getSubmissionRecordByReference).mockResolvedValue(null)

const response = await server.inject({
method: 'GET',
url: `/submission/${STUB_SUBMISSION_REF}`,
auth: authAdmin
})

expect(response.statusCode).toEqual(StatusCodes.NOT_FOUND)
})
})
})

/**
* @import { Server } from '@hapi/hapi'
*/
6 changes: 6 additions & 0 deletions src/services/submission-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,12 @@ function addFormComponentCellsToRow(formModel, row, context, record, options) {

addCellToRow(row, component.name, fileLinks, options)
addHeader(context, component)
} else if (component.type === ComponentType.GeospatialField) {
const features = record.data.main[component.name]
const value = JSON.stringify(features)

addCellToRow(row, component.name, value, options)
addHeader(context, component)
} else {
const value = getValue(record.data.main, key, component)

Expand Down
22 changes: 12 additions & 10 deletions src/services/submission-service.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,19 @@ describe('Submission service', () => {

const sheetAsCsv = xlsx.utils.sheet_to_csv(workbook.Sheets.Sheet1)

// XLSX escapes double quotes in JSON with another double quote
const geojson = `[{""id"":""6e9bd871-d9a9-44f8-a065-d02b30782c31"",""type"":""Feature"",""properties"":{""description"":""The quadrangle"",""gridReference"":""ST 000001""},""geometry"":{""coordinates"":[[[-0.14301008297061912,51.50124368719568],[-0.14226083598384776,51.501549070063504],[-0.14205122521880753,51.5013519595378],[-0.1420913634506462,51.501329749847514],[-0.141948649739021,51.501193715259205],[-0.1418995919000281,51.50120482013867],[-0.14168106152902737,51.50100493189194],[-0.14244641795605162,51.500686867437906],[-0.14301008297061912,51.50124368719568]]],""type"":""Polygon""}},{""id"":""36deedeb-54e6-4abf-a32e-233acf35f0cb"",""type"":""Feature"",""properties"":{""description"":""Birdcage walk"",""gridReference"":""ST 000002""},""geometry"":{""coordinates"":[[-0.14075460239675408,51.5002266684821],[-0.12978924563600458,51.50130199419283],[-0.14012872586908998,51.502050031896914]],""type"":""LineString""}},{""type"":""Feature"",""properties"":{""description"":""St James' park"",""gridReference"":""ST 000003""},""geometry"":{""type"":""Point"",""coordinates"":[-0.13359457492299498,51.5026889710461]},""id"":""ea4d3d46-64ac-4f31-87f4-8aa0d8b6ad96""}]`
expect(sheetAsCsv).toBe(
`Submission reference number,Submission date,Live or draft,Is preview,Easter egg,Your email,Country,Phone number,Delivery address,Fave color,Leading space,Pizza flavour 1,Quantity 1,Pizza flavour 2,Quantity 2,Pizza flavour 3,Quantity 3,Pizza flavour 4,Quantity 4,Files,Your email
365-DFR-C67,13/11/2025,live,No,Chocolate,,,,,,,,,,,,,,,,
549-FBF-C88,13/11/2025,live,No,Chocolate,,,,,,,,,,,,,,,,
187-231-E68,27/11/2025,draft,Yes,Chocolate,enrique.chase@defra.gov.uk,A,12345,"House name, Forest Hill, Village, Town, M15 5TX","A, C",,,,,,,,,,,d@s.com
259-0B2-442,28/11/2025,draft,Yes,Chocolate,enrique.chase@defra.gov.uk,B,123456789,"House name, Forest Hill, Village, Town, M15 5TX",A,,Cheese,2,Ham,6,,,,,,d@s.com
F6C-807-B1F,28/11/2025,draft,Yes,Kinder,kinder@egg.com,D,123,"Prime Minister & First Lord Of The Treasury 10, Downing Street, London, SW1A 2AA","A, B, C",,Ham,2,Cheese,1,Hawaian,12,,,,d@s.com
D44-841-706,28/11/2025,draft,Yes,Chocolate,kinder@egg.com,A,12345,"House name, Forest Hill, Village, Town, M15 5TX","A, B",,Egg,1,Ham,2,Bacon,4,,,http://localhost:3000/file-download/4444ac6f-7a5c-4bb8-bbd8-459c3700a42e,
8CC-882-665,28/11/2025,draft,Yes,Chocolate,kinder@egg.com,A,123456789,"House name, Forest Hill, Village, Town, M15 5TX","A, C",With leading space,Cheese,2,Hawaian,12,Cheese,6,,,http://localhost:3000/file-download/99d51a43-8121-4368-8b52-1ae93ebb9b61,
450-904-A2C,01/12/2025,draft,Yes,Chocolate,enrique.chase@defra.gov.uk,D,+447930696579,"Prime Minister & First Lord Of The Treasury 10, Downing Street, London, SW1A 2AA","A, C",,Ham,2,Pineapple,1,Bacon,5,Cheese,3,http://localhost:3000/file-download/207a6520-f311-4862-9d46-360d14918b4f,
8C2-7E8-189,02/12/2025,draft,Yes,Chocolate,kinder@egg.com,E,12345,"Orchards, Forest Hill, Village, Town, M15 5TX","A, C",With leading space,Egg,9,,,,,,,http://localhost:3000/file-download/e0f661ac-e9be-44ed-a156-e9128a89ce47,`
`Submission reference number,Submission date,Live or draft,Is preview,Easter egg,Your email,Country,Phone number,Delivery address,Fave color,Leading space,Site features,Pizza flavour 1,Quantity 1,Pizza flavour 2,Quantity 2,Pizza flavour 3,Quantity 3,Pizza flavour 4,Quantity 4,Files,Your email
365-DFR-C67,13/11/2025,live,No,Chocolate,,,,,,,[],,,,,,,,,,
549-FBF-C88,13/11/2025,live,No,Chocolate,,,,,,,[],,,,,,,,,,
187-231-E68,27/11/2025,draft,Yes,Chocolate,enrique.chase@defra.gov.uk,A,12345,"House name, Forest Hill, Village, Town, M15 5TX","A, C",,[],,,,,,,,,,d@s.com
259-0B2-442,28/11/2025,draft,Yes,Chocolate,enrique.chase@defra.gov.uk,B,123456789,"House name, Forest Hill, Village, Town, M15 5TX",A,,"${geojson}",Cheese,2,Ham,6,,,,,,d@s.com
F6C-807-B1F,28/11/2025,draft,Yes,Kinder,kinder@egg.com,D,123,"Prime Minister & First Lord Of The Treasury 10, Downing Street, London, SW1A 2AA","A, B, C",,[],Ham,2,Cheese,1,Hawaian,12,,,,d@s.com
D44-841-706,28/11/2025,draft,Yes,Chocolate,kinder@egg.com,A,12345,"House name, Forest Hill, Village, Town, M15 5TX","A, B",,[],Egg,1,Ham,2,Bacon,4,,,http://localhost:3000/file-download/4444ac6f-7a5c-4bb8-bbd8-459c3700a42e,
8CC-882-665,28/11/2025,draft,Yes,Chocolate,kinder@egg.com,A,123456789,"House name, Forest Hill, Village, Town, M15 5TX","A, C",With leading space,[],Cheese,2,Hawaian,12,Cheese,6,,,http://localhost:3000/file-download/99d51a43-8121-4368-8b52-1ae93ebb9b61,
450-904-A2C,01/12/2025,draft,Yes,Chocolate,enrique.chase@defra.gov.uk,D,+447930696579,"Prime Minister & First Lord Of The Treasury 10, Downing Street, London, SW1A 2AA","A, C",,[],Ham,2,Pineapple,1,Bacon,5,Cheese,3,http://localhost:3000/file-download/207a6520-f311-4862-9d46-360d14918b4f,
8C2-7E8-189,02/12/2025,draft,Yes,Chocolate,kinder@egg.com,E,12345,"Orchards, Forest Hill, Village, Town, M15 5TX","A, C",With leading space,[],Egg,9,,,,,,,http://localhost:3000/file-download/e0f661ac-e9be-44ed-a156-e9128a89ce47,`
)

expect(sendNotification).toHaveBeenCalledWith({
Expand Down
Loading
Loading