@defra/forms-engine-plugin 5.0.0-alpha.20 → 5.0.0-alpha.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.server/server/plugins/engine/helpers.d.ts +1 -0
- package/.server/server/plugins/engine/models/FormModel.js +16 -6
- package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
- package/.server/server/plugins/engine/models/types.d.ts +28 -1
- package/.server/server/plugins/engine/models/types.js.map +1 -1
- package/.server/server/plugins/engine/options.js +2 -1
- package/.server/server/plugins/engine/options.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/adapter/v1.js +11 -1
- package/.server/server/plugins/engine/outputFormatters/adapter/v1.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/helpers/submission.d.ts +34 -1
- package/.server/server/plugins/engine/pageControllers/helpers/submission.js +97 -0
- package/.server/server/plugins/engine/pageControllers/helpers/submission.js.map +1 -1
- package/.server/server/plugins/engine/plugin.js +3 -0
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/referenceNumbers.d.ts +1 -0
- package/.server/server/plugins/engine/referenceNumbers.js +1 -0
- package/.server/server/plugins/engine/referenceNumbers.js.map +1 -1
- package/.server/server/plugins/engine/routes/index.js +9 -4
- package/.server/server/plugins/engine/routes/index.js.map +1 -1
- package/.server/server/plugins/engine/types/enums.d.ts +3 -3
- package/.server/server/plugins/engine/types/enums.js +5 -4
- package/.server/server/plugins/engine/types/enums.js.map +1 -1
- package/.server/server/plugins/engine/types/schema.js +9 -3
- package/.server/server/plugins/engine/types/schema.js.map +1 -1
- package/.server/server/plugins/engine/types.d.ts +15 -2
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/typings/hapi/index.d.js.map +1 -1
- package/package.json +2 -2
- package/src/server/plugins/engine/models/FormModel.ts +25 -6
- package/src/server/plugins/engine/models/types.ts +31 -0
- package/src/server/plugins/engine/options.js +2 -1
- package/src/server/plugins/engine/outputFormatters/adapter/v1.ts +15 -1
- package/src/server/plugins/engine/pageControllers/helpers/submission.ts +137 -1
- package/src/server/plugins/engine/plugin.ts +6 -0
- package/src/server/plugins/engine/referenceNumbers.ts +2 -0
- package/src/server/plugins/engine/routes/index.ts +9 -3
- package/src/server/plugins/engine/types/enums.ts +4 -3
- package/src/server/plugins/engine/types/schema.ts +16 -4
- package/src/server/plugins/engine/types.ts +19 -1
- package/src/typings/hapi/index.d.ts +2 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ComponentType,
|
|
3
|
+
ConditionEvaluationOutcome,
|
|
3
4
|
ConditionsModel,
|
|
4
5
|
ControllerPath,
|
|
5
6
|
ControllerType,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
formDefinitionSchema,
|
|
9
10
|
formDefinitionV2Schema,
|
|
10
11
|
generateConditionAlias,
|
|
12
|
+
getErrorMessage,
|
|
11
13
|
hasComponents,
|
|
12
14
|
hasComponentsEvenIfNoNext,
|
|
13
15
|
hasRepeater,
|
|
@@ -54,7 +56,10 @@ import { extractBaseTranslations } from '../i18n/extractBaseTranslations.js'
|
|
|
54
56
|
import { createFormI18nInstance } from '../i18n/index.js'
|
|
55
57
|
import { getAvailableLanguages } from '../i18n/languages.js'
|
|
56
58
|
import { type Translator } from '../i18n/types.js'
|
|
57
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
type ConditionEvaluation,
|
|
61
|
+
type ExecutableCondition
|
|
62
|
+
} from './types.js'
|
|
58
63
|
import { type PageController } from '../pageControllers/PageController.js'
|
|
59
64
|
import {
|
|
60
65
|
createPage,
|
|
@@ -304,21 +309,35 @@ export class FormModel {
|
|
|
304
309
|
throw new ConditionBuildError(displayName, { cause })
|
|
305
310
|
}
|
|
306
311
|
|
|
307
|
-
const
|
|
312
|
+
const evaluate = (evaluationState: FormState): ConditionEvaluation => {
|
|
308
313
|
const ctx = this.toConditionContext(evaluationState, this.conditions)
|
|
314
|
+
|
|
309
315
|
try {
|
|
310
|
-
return
|
|
311
|
-
|
|
312
|
-
|
|
316
|
+
return {
|
|
317
|
+
outcome: (expr.evaluate(ctx) as boolean)
|
|
318
|
+
? ConditionEvaluationOutcome.True
|
|
319
|
+
: ConditionEvaluationOutcome.False
|
|
320
|
+
}
|
|
321
|
+
} catch (err) {
|
|
322
|
+
return {
|
|
323
|
+
outcome: ConditionEvaluationOutcome.Error,
|
|
324
|
+
error: getErrorMessage(err)
|
|
325
|
+
}
|
|
313
326
|
}
|
|
314
327
|
}
|
|
315
328
|
|
|
329
|
+
// A failed evaluation continues to route as `false`. `evaluate` exists so
|
|
330
|
+
// that the two can be told apart when recording outcomes for submission.
|
|
331
|
+
const fn = (evaluationState: FormState) =>
|
|
332
|
+
evaluate(evaluationState).outcome === ConditionEvaluationOutcome.True
|
|
333
|
+
|
|
316
334
|
return {
|
|
317
335
|
name,
|
|
318
336
|
displayName,
|
|
319
337
|
value,
|
|
320
338
|
expr,
|
|
321
|
-
fn
|
|
339
|
+
fn,
|
|
340
|
+
evaluate
|
|
322
341
|
}
|
|
323
342
|
}
|
|
324
343
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type ConditionEvaluationOutcome,
|
|
2
3
|
type ConditionWrapper,
|
|
3
4
|
type FormComponentsDef,
|
|
4
5
|
type Section
|
|
@@ -16,9 +17,39 @@ import {
|
|
|
16
17
|
type FormSubmissionError
|
|
17
18
|
} from '../types.js'
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The result of evaluating a condition, keeping a failed evaluation distinct
|
|
22
|
+
* from one that legitimately returned `false`.
|
|
23
|
+
* @see {@link ExecutableCondition.evaluate}
|
|
24
|
+
*/
|
|
25
|
+
export interface ConditionEvaluation {
|
|
26
|
+
outcome: ConditionEvaluationOutcome
|
|
27
|
+
error?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A form condition paired with the parsed expression and callbacks needed to
|
|
32
|
+
* run it against a form submission state
|
|
33
|
+
* Created by `FormModel.makeCondition`
|
|
34
|
+
*/
|
|
19
35
|
export type ExecutableCondition = ConditionWrapper & {
|
|
36
|
+
/**
|
|
37
|
+
* Parsed expression for the condition's {@link ConditionWrapper.value},
|
|
38
|
+
* evaluated against a context built from the submission state
|
|
39
|
+
*/
|
|
20
40
|
expr: Expression
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Evaluates the condition, used for page routing and component visibility
|
|
44
|
+
* A failed evaluation is reported as `false`
|
|
45
|
+
*/
|
|
21
46
|
fn: (evaluationState: FormState) => boolean
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* As `fn`, but reports whether evaluation failed rather than defaulting a
|
|
50
|
+
* failure to `false`. Used to record condition outcomes on submission.
|
|
51
|
+
*/
|
|
52
|
+
evaluate: (evaluationState: FormState) => ConditionEvaluation
|
|
22
53
|
}
|
|
23
54
|
|
|
24
55
|
/**
|
|
@@ -26,7 +26,8 @@ const pluginRegistrationOptionsSchema = Joi.object({
|
|
|
26
26
|
saveAndExit: Joi.function().optional(),
|
|
27
27
|
ordnanceSurveyApiKey: Joi.string().optional(),
|
|
28
28
|
ordnanceSurveyApiSecret: Joi.string().optional(),
|
|
29
|
-
getLanguage: Joi.function().optional()
|
|
29
|
+
getLanguage: Joi.function().optional(),
|
|
30
|
+
generateReferenceNumber: Joi.function().optional()
|
|
30
31
|
})
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Engine,
|
|
2
3
|
type FormMetadata,
|
|
3
4
|
type SubmitResponsePayload
|
|
4
5
|
} from '@defra/forms-model'
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
import { type FormModel } from '../../models/FormModel.js'
|
|
12
13
|
import { type DetailItem } from '../../models/types.js'
|
|
13
14
|
import { categoriseData } from '../machine/v2.js'
|
|
15
|
+
import { buildConditionEvaluations } from '../../pageControllers/helpers/submission.js'
|
|
14
16
|
import { FormAdapterSubmissionSchemaVersion } from '../../types/enums.js'
|
|
15
17
|
import {
|
|
16
18
|
type FormAdapterSubmissionMessageData,
|
|
@@ -73,7 +75,19 @@ export function format(
|
|
|
73
75
|
const payload: FormAdapterSubmissionMessagePayload = {
|
|
74
76
|
meta,
|
|
75
77
|
data,
|
|
76
|
-
result
|
|
78
|
+
result,
|
|
79
|
+
|
|
80
|
+
// Recorded here because only the engine holds the walked evaluation state
|
|
81
|
+
// the conditions were judged against; forms-notify-listener receives the
|
|
82
|
+
// flat submitted answers and resolves the outputs that qualify from these
|
|
83
|
+
// outcomes alone. Condition ids are only stable in V2, so a V1 definition
|
|
84
|
+
// has nothing to report - the property is still emitted, because its
|
|
85
|
+
// absence is what marks a message as predating this and sends the listener
|
|
86
|
+
// down its legacy path.
|
|
87
|
+
conditionEvaluations:
|
|
88
|
+
model.engine === Engine.V2
|
|
89
|
+
? buildConditionEvaluations(model, context)
|
|
90
|
+
: []
|
|
77
91
|
}
|
|
78
92
|
|
|
79
93
|
return JSON.stringify(payload)
|
|
@@ -1,13 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isConditionWrapperV2,
|
|
3
|
+
type ConditionDataV2,
|
|
4
|
+
type ConditionRefDataV2,
|
|
5
|
+
type ConditionWrapperV2,
|
|
6
|
+
type SubmitConditionEvaluation,
|
|
7
|
+
type SubmitConditionReference,
|
|
8
|
+
type SubmitPayload
|
|
9
|
+
} from '@defra/forms-model'
|
|
2
10
|
|
|
3
11
|
import { GeospatialField } from '../../components/GeospatialField.js'
|
|
4
12
|
import { PaymentField } from '../../components/PaymentField.js'
|
|
5
13
|
import { getAnswer } from '../../components/helpers/components.js'
|
|
6
14
|
import { type Translator } from '../../i18n/types.js'
|
|
15
|
+
import { type FormModel } from '../../models/FormModel.js'
|
|
7
16
|
import {
|
|
8
17
|
type DetailItem,
|
|
9
18
|
type DetailItemField
|
|
10
19
|
} from '../../models/types.js'
|
|
20
|
+
import {
|
|
21
|
+
type FormContext,
|
|
22
|
+
type FormState,
|
|
23
|
+
type FormStateValue
|
|
24
|
+
} from '../../types.js'
|
|
11
25
|
import {
|
|
12
26
|
formatCurrency,
|
|
13
27
|
formatPaymentDate
|
|
@@ -141,3 +155,125 @@ export function buildRepeaterRecords(
|
|
|
141
155
|
)
|
|
142
156
|
}))
|
|
143
157
|
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Records the outcome of every condition in the form definition, evaluated
|
|
161
|
+
* against the answers as they stand at the point of submission.
|
|
162
|
+
*
|
|
163
|
+
* Evaluated here rather than by the consumer because only the engine holds
|
|
164
|
+
* the context to evaluate against: `evaluationState` is built by walking the
|
|
165
|
+
* form from its start page along the path taken, applying page conditions as
|
|
166
|
+
* it goes. A consumer receives the flat submitted answers, not that walked
|
|
167
|
+
* state, and reproducing the walk in a second codebase would leave two
|
|
168
|
+
* implementations of the same logic free to diverge.
|
|
169
|
+
*
|
|
170
|
+
* Each record carries the components the condition depends on and whether each
|
|
171
|
+
* was answered. An unanswered question still yields a boolean - negative
|
|
172
|
+
* operators such as "is not" return `true` against the seeded `null` - so the
|
|
173
|
+
* outcome alone cannot be read as evidence that the user gave that answer.
|
|
174
|
+
*
|
|
175
|
+
* V2 definitions only. `conditionId` is the V2 condition id, and the
|
|
176
|
+
* references are resolved from the component ids V2 conditions carry - V1
|
|
177
|
+
* conditions reference components by name, and V1 components need not have
|
|
178
|
+
* an id at all.
|
|
179
|
+
*/
|
|
180
|
+
export function buildConditionEvaluations(
|
|
181
|
+
model: FormModel,
|
|
182
|
+
context: FormContext
|
|
183
|
+
): SubmitConditionEvaluation[] {
|
|
184
|
+
const { evaluationState } = context
|
|
185
|
+
|
|
186
|
+
return model.def.conditions
|
|
187
|
+
.filter(isConditionWrapperV2)
|
|
188
|
+
.flatMap((conditionDef) => {
|
|
189
|
+
const condition = model.conditions[conditionDef.id]
|
|
190
|
+
|
|
191
|
+
if (!condition) {
|
|
192
|
+
return []
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const { outcome } = condition.evaluate(evaluationState)
|
|
196
|
+
|
|
197
|
+
const references = collectReferences(model, conditionDef, evaluationState)
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
conditionId: conditionDef.id,
|
|
201
|
+
outcome,
|
|
202
|
+
references: [...references.values()]
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Whether a component held an answer at the point a condition was evaluated.
|
|
209
|
+
*
|
|
210
|
+
* The engine seeds every component in `evaluationState` with `null` before the
|
|
211
|
+
* page walk begins, so an unanswered question is present but empty rather than
|
|
212
|
+
* absent.
|
|
213
|
+
* @see {@link FormModel.initialiseContext}
|
|
214
|
+
*/
|
|
215
|
+
export function isAnswered(value: FormStateValue | undefined) {
|
|
216
|
+
if (value === undefined || value === null) {
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (Array.isArray(value)) {
|
|
221
|
+
return value.length > 0
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return value !== ''
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isConditionDataV2(
|
|
228
|
+
item: ConditionDataV2 | ConditionRefDataV2
|
|
229
|
+
): item is ConditionDataV2 {
|
|
230
|
+
return 'componentId' in item
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Recursive function that collects every component a condition depends on, following nested condition
|
|
235
|
+
* references. Results are keyed by component id so a component referenced more
|
|
236
|
+
* than once is reported once.
|
|
237
|
+
*/
|
|
238
|
+
function collectReferences(
|
|
239
|
+
model: FormModel,
|
|
240
|
+
conditionDef: ConditionWrapperV2,
|
|
241
|
+
evaluationState: FormState,
|
|
242
|
+
references: Map<string, SubmitConditionReference> = new Map<
|
|
243
|
+
string,
|
|
244
|
+
SubmitConditionReference
|
|
245
|
+
>(),
|
|
246
|
+
visited: Set<string> = new Set<string>()
|
|
247
|
+
) {
|
|
248
|
+
if (visited.has(conditionDef.id)) {
|
|
249
|
+
return references
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
visited.add(conditionDef.id)
|
|
253
|
+
|
|
254
|
+
for (const item of conditionDef.items) {
|
|
255
|
+
if (isConditionDataV2(item)) {
|
|
256
|
+
const component = model.getComponentById(item.componentId)
|
|
257
|
+
|
|
258
|
+
// A condition referencing a component that no longer exists cannot be
|
|
259
|
+
// resolved to a name, so there is nothing meaningful to report for it
|
|
260
|
+
if (component) {
|
|
261
|
+
references.set(item.componentId, {
|
|
262
|
+
componentId: item.componentId,
|
|
263
|
+
componentName: component.name,
|
|
264
|
+
answered: isAnswered(evaluationState[component.name])
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
continue
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const referenced = model.getConditionById(item.conditionId)
|
|
272
|
+
|
|
273
|
+
if (referenced) {
|
|
274
|
+
collectReferences(model, referenced, evaluationState, references, visited)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return references
|
|
279
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
|
|
9
9
|
import { type FormModel } from './models/index.js'
|
|
10
10
|
import { validatePluginOptions } from './options.js'
|
|
11
|
+
import { generateUniqueReference } from './referenceNumbers.js'
|
|
11
12
|
import { getRoutes as getFileUploadStatusRoutes } from './routes/file-upload.js'
|
|
12
13
|
import { makeLoadFormPreHandler } from './routes/index.js'
|
|
13
14
|
import { getRoutes as getPaymentRoutes } from './routes/payment.js'
|
|
@@ -37,6 +38,7 @@ export const plugin = {
|
|
|
37
38
|
cache,
|
|
38
39
|
saveAndExit,
|
|
39
40
|
getLanguage,
|
|
41
|
+
generateReferenceNumber,
|
|
40
42
|
nunjucks: nunjucksOptions,
|
|
41
43
|
viewContext,
|
|
42
44
|
preparePageEventRequestOptions,
|
|
@@ -80,6 +82,10 @@ export const plugin = {
|
|
|
80
82
|
server.expose('cacheService', cacheService)
|
|
81
83
|
server.expose('saveAndExit', saveAndExit)
|
|
82
84
|
server.expose('getLanguage', getLanguage)
|
|
85
|
+
server.expose(
|
|
86
|
+
'generateReferenceNumber',
|
|
87
|
+
generateReferenceNumber ?? generateUniqueReference
|
|
88
|
+
)
|
|
83
89
|
server.expose('baseUrl', baseUrl)
|
|
84
90
|
server.expose('services', services)
|
|
85
91
|
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
englishRecommendedTransformers
|
|
7
7
|
} from 'obscenity'
|
|
8
8
|
|
|
9
|
+
export const DEFAULT_REFERENCE_NUMBER = 'XXX-XXX-XXX'
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed.
|
|
11
13
|
* @param strCodes - array of binary input values
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
findPage,
|
|
21
21
|
getCacheService,
|
|
22
22
|
getPage,
|
|
23
|
+
getPluginOptions,
|
|
23
24
|
getStartPath,
|
|
24
25
|
proceed
|
|
25
26
|
} from '../helpers.js'
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
checkSaveAndExitRepeater,
|
|
29
30
|
copyNotYetValidatedState
|
|
30
31
|
} from '../pageControllers/helpers/state.js'
|
|
31
|
-
import {
|
|
32
|
+
import { DEFAULT_REFERENCE_NUMBER } from '../referenceNumbers.js'
|
|
32
33
|
import * as defaultServices from '../services/index.js'
|
|
33
34
|
import {
|
|
34
35
|
type AnyFormRequest,
|
|
@@ -54,7 +55,7 @@ export async function redirectOrMakeHandler(
|
|
|
54
55
|
context: FormContext
|
|
55
56
|
) => ResponseObject | Promise<ResponseObject>
|
|
56
57
|
) {
|
|
57
|
-
const { app, params } = request
|
|
58
|
+
const { app, params, query } = request
|
|
58
59
|
const { model } = app
|
|
59
60
|
|
|
60
61
|
if (!model) {
|
|
@@ -74,7 +75,12 @@ export async function redirectOrMakeHandler(
|
|
|
74
75
|
)
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
const
|
|
78
|
+
const isForce = 'force' in query
|
|
79
|
+
const { generateReferenceNumber } = getPluginOptions(request.server)
|
|
80
|
+
const referenceNumber = isForce
|
|
81
|
+
? DEFAULT_REFERENCE_NUMBER
|
|
82
|
+
: await generateReferenceNumber(prefix)
|
|
83
|
+
|
|
78
84
|
state = await page.mergeState(request, state, {
|
|
79
85
|
$$__referenceNumber: referenceNumber
|
|
80
86
|
})
|
|
@@ -10,6 +10,7 @@ export enum FileStatus {
|
|
|
10
10
|
pending = 'pending'
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
// Changed from an enum to enforce numeric values
|
|
14
|
+
export const FormAdapterSubmissionSchemaVersion = {
|
|
15
|
+
V1: 1
|
|
16
|
+
} as const satisfies Record<string, number>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
FormStatus,
|
|
3
|
+
formSubmitConditionEvaluationSchema,
|
|
3
4
|
formVersionMetadataSchema,
|
|
4
5
|
idSchema,
|
|
5
6
|
notificationEmailAddressSchema,
|
|
@@ -18,9 +19,9 @@ import {
|
|
|
18
19
|
|
|
19
20
|
export const formAdapterSubmissionMessageMetaSchema =
|
|
20
21
|
Joi.object<FormAdapterSubmissionMessageMeta>().keys({
|
|
21
|
-
schemaVersion: Joi.
|
|
22
|
-
...Object.values(FormAdapterSubmissionSchemaVersion)
|
|
23
|
-
|
|
22
|
+
schemaVersion: Joi.number()
|
|
23
|
+
.valid(...Object.values(FormAdapterSubmissionSchemaVersion))
|
|
24
|
+
.required(),
|
|
24
25
|
timestamp: Joi.date().required(),
|
|
25
26
|
referenceNumber: Joi.string().required(),
|
|
26
27
|
formName: titleSchema,
|
|
@@ -80,5 +81,16 @@ export const formAdapterSubmissionMessagePayloadSchema =
|
|
|
80
81
|
Joi.object<FormAdapterSubmissionMessagePayload>().keys({
|
|
81
82
|
meta: formAdapterSubmissionMessageMetaSchema.required(),
|
|
82
83
|
data: formAdapterSubmissionMessageDataSchema.required(),
|
|
83
|
-
result: formAdapterSubmissionMessageResultSchema.required()
|
|
84
|
+
result: formAdapterSubmissionMessageResultSchema.required(),
|
|
85
|
+
// Optional so that messages published before this existed - which may
|
|
86
|
+
// still be in flight or sitting on a dead-letter queue - continue to
|
|
87
|
+
// validate. Consumers treat its absence as "resolve the recipients from
|
|
88
|
+
// the form definition yourself"
|
|
89
|
+
// This should be required at a later point in time.
|
|
90
|
+
conditionEvaluations: Joi.array()
|
|
91
|
+
.items(formSubmitConditionEvaluationSchema)
|
|
92
|
+
.optional()
|
|
93
|
+
.description(
|
|
94
|
+
'Outcome of every condition in the form definition, evaluated against the final answers at submission'
|
|
95
|
+
)
|
|
84
96
|
})
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type List,
|
|
7
7
|
type Page,
|
|
8
8
|
type PaymentFieldComponent,
|
|
9
|
+
type SubmitConditionEvaluation,
|
|
9
10
|
type UkAddressFieldComponent
|
|
10
11
|
} from '@defra/forms-model'
|
|
11
12
|
import {
|
|
@@ -483,6 +484,10 @@ export type SaveAndExitHandler = (
|
|
|
483
484
|
|
|
484
485
|
export type GetLanguageHandler = (query?: RequestQuery, yar?: Yar) => string
|
|
485
486
|
|
|
487
|
+
export type GenerateReferenceNumberHandler = (
|
|
488
|
+
prefix?: string
|
|
489
|
+
) => Promise<string>
|
|
490
|
+
|
|
486
491
|
export interface ExternalArgs {
|
|
487
492
|
component: ComponentDef
|
|
488
493
|
controller: QuestionPageController
|
|
@@ -523,13 +528,14 @@ export interface PluginOptions {
|
|
|
523
528
|
preparePageEventRequestOptions?: PreparePageEventRequestOptions
|
|
524
529
|
onRequest?: OnRequestCallback
|
|
525
530
|
getLanguage?: GetLanguageHandler
|
|
531
|
+
generateReferenceNumber?: GenerateReferenceNumberHandler
|
|
526
532
|
baseUrl: string // base URL of the application, protocol and hostname e.g. "https://myapp.com"
|
|
527
533
|
ordnanceSurveyApiKey?: string
|
|
528
534
|
ordnanceSurveyApiSecret?: string
|
|
529
535
|
}
|
|
530
536
|
|
|
531
537
|
export interface FormAdapterSubmissionMessageMeta {
|
|
532
|
-
schemaVersion: FormAdapterSubmissionSchemaVersion
|
|
538
|
+
schemaVersion: (typeof FormAdapterSubmissionSchemaVersion)[keyof typeof FormAdapterSubmissionSchemaVersion]
|
|
533
539
|
timestamp: Date
|
|
534
540
|
referenceNumber: string
|
|
535
541
|
formName: string
|
|
@@ -607,6 +613,18 @@ export interface FormAdapterSubmissionMessagePayload {
|
|
|
607
613
|
meta: FormAdapterSubmissionMessageMeta
|
|
608
614
|
data: FormAdapterSubmissionMessageData
|
|
609
615
|
result: FormAdapterSubmissionMessageResult
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* The outcome of every condition in the form definition, evaluated against
|
|
619
|
+
* the final answers at the point of submission.
|
|
620
|
+
*
|
|
621
|
+
* Always emitted, empty when there is nothing to report - a V1-engine form
|
|
622
|
+
* has no stable condition ids to record against. Optional only for
|
|
623
|
+
* backwards compatibility: a message published before this existed carries
|
|
624
|
+
* no evaluations at all, and consumers fall back to resolving the recipients
|
|
625
|
+
* from the form definition themselves.
|
|
626
|
+
*/
|
|
627
|
+
conditionEvaluations?: SubmitConditionEvaluation[]
|
|
610
628
|
}
|
|
611
629
|
|
|
612
630
|
export interface FormAdapterSubmissionMessage extends FormAdapterSubmissionMessagePayload {
|
|
@@ -11,6 +11,7 @@ import { type FormModel } from '../../server/plugins/engine/models/index.js'
|
|
|
11
11
|
import {
|
|
12
12
|
type AnyFormRequest,
|
|
13
13
|
type FormSubmissionError,
|
|
14
|
+
type GenerateReferenceNumberHandler,
|
|
14
15
|
type PluginOptions
|
|
15
16
|
} from '../../server/plugins/engine/types.ts'
|
|
16
17
|
import { type CacheService } from '../../server/services/index.js'
|
|
@@ -41,6 +42,7 @@ declare module '@hapi/hapi' {
|
|
|
41
42
|
getLanguage?: PluginOptions['getLanguage']
|
|
42
43
|
baseUrl: string
|
|
43
44
|
services: PluginOptions['services']
|
|
45
|
+
generateReferenceNumber: GenerateReferenceNumberHandler
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|