Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,763 changes: 4,766 additions & 1,997 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
"setup:husky": "node -e \"try { (await import('husky')).default() } catch (e) { if (e.code !== 'ERR_MODULE_NOT_FOUND') throw e }\" --input-type module"
},
"dependencies": {
"@aws-sdk/client-sqs": "3.894.0",
"@defra/forms-engine-plugin": "4.0.33",
"@defra/forms-model": "3.0.604",
"@aws-sdk/client-sqs": "3.982.0",
"@defra/forms-engine-plugin": "4.0.44",
"@defra/forms-model": "3.0.611",
"@defra/hapi-tracing": "^1.28.0",
"@elastic/ecs-pino-format": "^1.5.0",
"@hapi/hapi": "^21.4.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`Page controller helpers format should return a valid human readable v1 response 1`] = `
"^ For security reasons, the links in this email expire at 12:00am on Tuesday 1 July 2025
Expand Down
166 changes: 114 additions & 52 deletions src/service/mappers/formatters/human/v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { format as dateFormat } from '~/src/helpers/date.js'
import { stringHasNonEmptyValue } from '~/src/helpers/string-utils.js'
import { escapeContent, escapeFileLabel } from '~/src/lib/notify.js'
import {
extractPaymentDetails,
findRepeaterPageByKey,
formatLocationField,
formatMultilineTextField,
Expand All @@ -38,55 +39,37 @@ export function handleReferenceNumber(definition, message, lines) {
}

/**
* Human readable notify formatter v1
* Appends the payment details section to the email lines if payment exists
* @param {FormAdapterSubmissionMessage} formSubmissionMessage
* @param {FormDefinition} formDefinition
* @param {string} _schemaVersion
* @param {string[]} lines
*/
export function formatter(
formSubmissionMessage,
formDefinition,
_schemaVersion
) {
const { meta, result } = formSubmissionMessage
const { isPreview, status } = meta
const files = result.files
function appendPaymentSection(formSubmissionMessage, lines) {
const paymentDetails = extractPaymentDetails(formSubmissionMessage)

const formModel = new FormModel(formDefinition, { basePath: '' }, {})

const formName = escapeContent(meta.formName)
/**
* @todo Refactor this below but the code to
* generate the question and answers works for now
*/
const now = new Date()
const formattedNow = `${dateFormat(now, 'h:mmaaa')} on ${dateFormat(now, 'd MMMM yyyy')}`

const fileExpiryDate = addDays(now, FILE_EXPIRY_OFFSET)
const formattedExpiryDate = `${dateFormat(fileExpiryDate, 'h:mmaaa')} on ${dateFormat(fileExpiryDate, 'eeee d MMMM yyyy')}`

const order = calculateOrder(formDefinition, formSubmissionMessage)
const componentMap = new Map()
/**
* @type {string[]}
*/
const lines = []

lines.push(
`^ For security reasons, the links in this email expire at ${escapeContent(formattedExpiryDate)}\n`
)

if (isPreview) {
lines.push(`This is a test of the ${formName} ${status} form.\n`)
if (!paymentDetails) {
return
}

lines.push(
`${formName} form received at ${escapeContent(formattedNow)}.\n`,
'---\n',
'# Payment details\n',
'## Payment for\n',
`${escapeContent(paymentDetails.description)}\n`,
'## Total amount\n',
`£${paymentDetails.amount}\n`,
'## Date of payment\n',
`${escapeContent(paymentDetails.dateOfPayment)}\n`,
'---\n'
)
}

handleReferenceNumber(formDefinition, formSubmissionMessage, lines)

/**
* Process main form entries and add them to the component map
* @param {FormAdapterSubmissionMessage} formSubmissionMessage
* @param {FormModel} formModel
* @param {Map<string, string[]>} componentMap
*/
function processMainEntries(formSubmissionMessage, formModel, componentMap) {
const mainEntries = Object.entries({
...formSubmissionMessage.data.main,
...formSubmissionMessage.data.files
Expand Down Expand Up @@ -115,44 +98,123 @@ export function formatter(
questionLines.push('---\n')
componentMap.set(key, questionLines)
}
}

/**
* Process repeater entries and add them to the component map
* @param {FormAdapterSubmissionMessage} formSubmissionMessage
* @param {FormDefinition} formDefinition
* @param {Map<string, string[]>} componentMap
*/
function processRepeaterEntries(
formSubmissionMessage,
formDefinition,
componentMap
) {
const repeaterEntries = Object.entries(
formSubmissionMessage.result.files.repeaters
)

for (const [key, fileId] of repeaterEntries) {
const repeaterPage = findRepeaterPageByKey(key, formDefinition)

const questionLines = /** @type {string[]} */ ([])
if (!hasRepeater(repeaterPage)) {
continue
}

if (hasRepeater(repeaterPage)) {
const label = escapeContent(repeaterPage.repeat.options.title)
const componentKey = repeaterPage.repeat.options.name
const label = escapeContent(repeaterPage.repeat.options.title)
const componentKey = repeaterPage.repeat.options.name
const questionLines = /** @type {string[]} */ ([])

questionLines.push(`## ${label}\n`)
questionLines.push(`## ${label}\n`)

const repeaterFilename = escapeFileLabel(`Download ${label} (CSV)`)
questionLines.push(
`[${repeaterFilename}](${designerUrl}/file-download/${fileId})\n`,
'---\n'
)
componentMap.set(componentKey, questionLines)
}
const repeaterFilename = escapeFileLabel(`Download ${label} (CSV)`)
questionLines.push(
`[${repeaterFilename}](${designerUrl}/file-download/${fileId})\n`,
'---\n'
)
componentMap.set(componentKey, questionLines)
}
}

/**
* Append component lines to the output in the correct order
* @param {string[]} order
* @param {Map<string, string[]>} componentMap
* @param {string[]} lines
*/
function appendComponentLines(order, componentMap, lines) {
for (const key of order) {
const componentLines = componentMap.get(key)

if (componentLines) {
lines.push(...componentLines)
}
}
}

/**
* Human readable notify formatter v1
* @param {FormAdapterSubmissionMessage} formSubmissionMessage
* @param {FormDefinition} formDefinition
* @param {string} _schemaVersion
*/
export function formatter(
formSubmissionMessage,
formDefinition,
_schemaVersion
) {
const { meta, result } = formSubmissionMessage
const { isPreview, status } = meta
const files = result.files

const formModel = new FormModel(formDefinition, { basePath: '' }, {})

const formName = escapeContent(meta.formName)
/**
* @todo Refactor this below but the code to
* generate the question and answers works for now
*/
const now = new Date()
const formattedNow = `${dateFormat(now, 'h:mmaaa')} on ${dateFormat(now, 'd MMMM yyyy')}`

const fileExpiryDate = addDays(now, FILE_EXPIRY_OFFSET)
const formattedExpiryDate = `${dateFormat(fileExpiryDate, 'h:mmaaa')} on ${dateFormat(fileExpiryDate, 'eeee d MMMM yyyy')}`

const order = calculateOrder(formDefinition, formSubmissionMessage)
const componentMap = new Map()
/**
* @type {string[]}
*/
const lines = []

lines.push(
`^ For security reasons, the links in this email expire at ${escapeContent(formattedExpiryDate)}\n`
)

if (isPreview) {
lines.push(`This is a test of the ${formName} ${status} form.\n`)
}

lines.push(
`${formName} form received at ${escapeContent(formattedNow)}.\n`,
'---\n'
)

handleReferenceNumber(formDefinition, formSubmissionMessage, lines)

processMainEntries(formSubmissionMessage, formModel, componentMap)
processRepeaterEntries(formSubmissionMessage, formDefinition, componentMap)
appendComponentLines(order, componentMap, lines)

const mainResultFilename = escapeFileLabel('Download main form (CSV)')
lines.push(
`[${mainResultFilename}](${designerUrl}/file-download/${files.main})\n`
)

// Add payment details section if payment exists
appendPaymentSection(formSubmissionMessage, lines)

lines.push('\n', 'Thanks,', 'Defra')

return lines.join('\n')
Expand Down
86 changes: 86 additions & 0 deletions src/service/mappers/formatters/human/v1.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,90 @@ describe('Page controller helpers', () => {
])
})
})

describe('payment details', () => {
it('should include payment details section when payment exists', () => {
const definition = buildDefinition({
...exampleNotifyFormDefinition,
output: {
audience: 'human',
version: '1'
}
})

const messageWithPayment = buildFormAdapterSubmissionMessage({
...exampleNotifyFormMessage,
data: {
...exampleNotifyFormMessage.data,
payment: {
paymentId: 'pay_abc123',
reference: 'REF-123-456',
amount: 150,
description: 'Application fee',
createdAt: '2025-11-10T17:01:29.000Z'
}
}
})

const formatter = getFormatter('human', '1')
const output = formatter(messageWithPayment, definition, '1')

expect(output).toContain('# Payment details')
expect(output).toContain('## Payment for')
expect(output).toContain('Application fee')
expect(output).toContain('## Total amount')
expect(output).toContain('£150')
expect(output).toContain('## Date of payment')
expect(output).toContain('5:01pm on 10 November 2025')
})

it('should not include payment details section when no payment exists', () => {
const definition = buildDefinition({
...exampleNotifyFormDefinition,
output: {
audience: 'human',
version: '1'
}
})

const messageWithNoPayment = buildFormAdapterSubmissionMessage({
...exampleNotifyFormMessage,
data: {
...exampleNotifyFormMessage.data,
payment: undefined
}
})

const formatter = getFormatter('human', '1')
const output = formatter(messageWithNoPayment, definition, '1')

expect(output).not.toContain('# Payment details')
expect(output).not.toContain('## Payment for')
expect(output).not.toContain('## Total amount')
expect(output).not.toContain('## Date of payment')
})

it('should not include payment details section when payment is undefined', () => {
const definition = buildDefinition({
...exampleNotifyFormDefinition,
output: {
audience: 'human',
version: '1'
}
})

const messageWithNoPayment = buildFormAdapterSubmissionMessage({
...exampleNotifyFormMessage,
data: {
...exampleNotifyFormMessage.data,
payment: undefined
}
})

const formatter = getFormatter('human', '1')
const output = formatter(messageWithNoPayment, definition, '1')

expect(output).not.toContain('# Payment details')
})
})
})
26 changes: 25 additions & 1 deletion src/service/mappers/formatters/shared.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { hasRepeater } from '@defra/forms-model'

import { format as dateFormat } from '~/src/helpers/date.js'
import { escapeContent } from '~/src/lib/notify.js'

/**
Expand Down Expand Up @@ -56,8 +57,31 @@ export function formatLocationField(_answer, field, richFormValue) {
return contextValue ? `${contextValue}\n` : ''
}

/**
* Extracts payment details from the submission message if a payment exists.
* Forms only have one payment component.
* @param {FormAdapterSubmissionMessage} formSubmissionMessage
* @returns {{ description: string, amount: number, dateOfPayment: string } | undefined}
*/
export function extractPaymentDetails(formSubmissionMessage) {
const payment = formSubmissionMessage.data.payment

if (!payment) {
return undefined
}

const date = new Date(payment.createdAt)
const dateOfPayment = `${dateFormat(date, 'h:mmaaa')} on ${dateFormat(date, 'd MMMM yyyy')}`

return {
description: payment.description,
amount: payment.amount,
dateOfPayment
}
}

/**
* @import { Component } from '@defra/forms-engine-plugin/engine/components/helpers/components.js'
* @import { RichFormValue } from '@defra/forms-engine-plugin/engine/types.js'
* @import { FormAdapterSubmissionMessage, RichFormValue } from '@defra/forms-engine-plugin/engine/types.js'
* @import { FormDefinition } from '@defra/forms-model'
*/
Loading
Loading