Skip to content

Commit ced4842

Browse files
authored
minor: SummaryViewModel outputs (#1264)
* add tests for webhook model * move state for readability * add getRelevantPages to FormModel * add test for RepeatingFieldPageController * refactor: use model.getRelevantPages instead of SummaryViewModel * add new webhook model * add types * use flatmap to support nested fields * refactor: use new Outputs object * move payApiKey to summaryPageController * refactor: removing old WebhookModel * change summaryViewModel suite to SummaryPageController * remove fees from outputs * adding notes and tidying * only set outputs to SVM if no callback detected * rename tests * add test for page titles * fix test for page titles * fix function call
1 parent 03e19c8 commit ced4842

File tree

16 files changed

+1081
-206
lines changed

16 files changed

+1081
-206
lines changed

e2e/cypress/e2e/runner/initialiseSession.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,20 @@ When("the session is initialised with the options", (table) => {
1111
redirectUrl,
1212
},
1313
},
14-
questions: [],
14+
questions: [
15+
{
16+
question: "What is your name?",
17+
fields: [
18+
{
19+
key: "firstName",
20+
title: "What is your name?",
21+
type: "text",
22+
answer: "Jen",
23+
},
24+
],
25+
index: 0,
26+
},
27+
],
1528
}).then((res) => {
1629
cy.wrap(res.body.token).as("token");
1730
});

e2e/cypress/fixtures/initialiseSession.json

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,31 @@
77
"components": [],
88
"next": [
99
{
10-
"path": "/summary"
10+
"path": "/name"
1111
}
1212
],
1313
"controller": "./pages/start.js"
1414
},
15+
{
16+
"title": "What is your name?",
17+
"path": "/name",
18+
"components": [
19+
{
20+
"type": "TextField",
21+
"name": "firstName",
22+
"title": "First name",
23+
"options": {
24+
"required": true
25+
},
26+
"schema": {}
27+
}
28+
],
29+
"next": [
30+
{
31+
"path": "/summary"
32+
}
33+
]
34+
},
1535
{
1636
"path": "/summary",
1737
"controller": "./pages/summary.js",
@@ -20,14 +40,12 @@
2040
"next": []
2141
}
2242
],
23-
"lists": [
24-
],
43+
"lists": [],
2544
"sections": [],
2645
"phaseBanner": {},
2746
"fees": [],
2847
"payApiKey": "",
29-
"outputs": [
30-
],
48+
"outputs": [],
3149
"declaration": "<p class=\"govuk-body\">All the answers you have provided are true to the best of your knowledge.</p>",
3250
"version": 2,
3351
"conditions": []

model/src/data-model/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export type NotifyOutputConfiguration = {
9797
export type WebhookOutputConfiguration = {
9898
url: string;
9999
sendAdditionalPayMetadata?: boolean;
100+
allowRetry?: boolean;
100101
};
101102

102103
export type OutputConfiguration =
@@ -162,6 +163,7 @@ export type FeeOptions = {
162163
customPayErrorMessage?: string;
163164
showPaymentSkippedWarningPage: boolean;
164165
additionalReportingColumns?: AdditionalReportingColumn[];
166+
payApiKey?: string | MultipleApiKeys | undefined;
165167
};
166168

167169
/**

runner/src/server/plugins/engine/models/FormModel.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import {
1212
} from "@xgovformbuilder/model";
1313

1414
import { FormSubmissionState } from "../types";
15-
import { PageControllerBase, getPageController } from "../pageControllers";
15+
import {
16+
PageControllerBase,
17+
getPageController,
18+
SummaryPageController,
19+
} from "../pageControllers";
1620
import { PageController } from "../pageControllers/PageController";
1721
import { ExecutableCondition } from "server/plugins/engine/models/types";
1822
import { DEFAULT_FEE_OPTIONS } from "server/plugins/engine/models/FormModel.feeOptions";
@@ -255,4 +259,24 @@ export class FormModel {
255259
getContextState(state: FormSubmissionState) {
256260
return this.fieldsForContext.getFormDataFromState(state);
257261
}
262+
263+
getRelevantPages(state: FormSubmissionState) {
264+
let nextPage = this.startPage;
265+
const relevantPages: any[] = [];
266+
let endPage = null;
267+
268+
while (nextPage != null) {
269+
if (nextPage.hasFormComponents) {
270+
relevantPages.push(nextPage);
271+
} else if (
272+
!nextPage.hasNext &&
273+
!(nextPage instanceof SummaryPageController)
274+
) {
275+
endPage = nextPage;
276+
}
277+
nextPage = nextPage.getNextPage(state, true);
278+
}
279+
280+
return { relevantPages, endPage };
281+
}
258282
}

runner/src/server/plugins/engine/models/SummaryViewModel.ts

Lines changed: 8 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,12 @@ import { FormModel } from "./FormModel";
44
import { feedbackReturnInfoKey, redirectUrl } from "../helpers";
55
import { decodeFeedbackContextInfo } from "../feedback";
66
import { webhookSchema } from "server/schemas/webhookSchema";
7-
import { SummaryPageController } from "../pageControllers";
87
import { FormSubmissionState } from "../types";
98
import { FEEDBACK_CONTEXT_ITEMS, WebhookData } from "./types";
10-
import {
11-
EmailModel,
12-
FeesModel,
13-
NotifyModel,
14-
WebhookModel,
15-
} from "server/plugins/engine/models/submission";
16-
import { FormDefinition, isMultipleApiKey } from "@xgovformbuilder/model";
9+
import { FeesModel } from "server/plugins/engine/models/submission";
1710
import { HapiRequest } from "src/server/types";
1811
import { InitialiseSessionOptions } from "server/plugins/initialiseSession/types";
12+
import { Outputs } from "server/plugins/engine/models/submission/Outputs";
1913

2014
/**
2115
* TODO - extract submission behaviour dependencies from the viewmodel
@@ -55,7 +49,6 @@ export class SummaryViewModel {
5549
| undefined;
5650

5751
_outputs: any; // TODO
58-
_payApiKey: FormDefinition["payApiKey"];
5952
_webhookData: WebhookData | undefined;
6053
callback?: InitialiseSessionOptions;
6154
showPaymentSkippedWarningPage: boolean = false;
@@ -66,14 +59,13 @@ export class SummaryViewModel {
6659
request: HapiRequest
6760
) {
6861
this.pageTitle = pageTitle;
69-
const { relevantPages, endPage } = this.getRelevantPages(model, state);
62+
const { relevantPages, endPage } = model.getRelevantPages(state);
7063
const details = this.summaryDetails(request, model, state, relevantPages);
7164
const { def } = model;
7265
// @ts-ignore
7366
this.declaration = def.declaration;
7467
// @ts-ignore
7568
this.skipSummary = def.skipSummary;
76-
this._payApiKey = def.feeOptions?.payApiKey ?? def.payApiKey;
7769
this.endPage = endPage;
7870
this.feedbackLink =
7971
def.feedback?.url ??
@@ -92,14 +84,10 @@ export class SummaryViewModel {
9284
this.processErrors(result, details);
9385
} else {
9486
this.fees = FeesModel(model, state);
87+
const outputs = new Outputs(model, state);
9588

96-
this._webhookData = WebhookModel(
97-
relevantPages,
98-
details,
99-
model,
100-
this.fees,
101-
model.getContextState(state)
102-
);
89+
// TODO: move to controller
90+
this._webhookData = outputs.webhookData;
10391
this._webhookData = this.addFeedbackSourceDataToWebhook(
10492
this._webhookData,
10593
model,
@@ -111,40 +99,8 @@ export class SummaryViewModel {
11199
* Skip outputs if this is a callback
112100
*/
113101
if (def.outputs && !state.callback) {
114-
this._outputs = def.outputs.map((output) => {
115-
switch (output.type) {
116-
case "notify":
117-
return {
118-
type: "notify",
119-
outputData: NotifyModel(
120-
model,
121-
output.outputConfiguration,
122-
state
123-
),
124-
};
125-
case "email":
126-
return {
127-
type: "email",
128-
outputData: EmailModel(
129-
model,
130-
output.outputConfiguration,
131-
this._webhookData
132-
),
133-
};
134-
case "webhook":
135-
return {
136-
type: "webhook",
137-
outputData: {
138-
url: output.outputConfiguration.url,
139-
sendAdditionalPayMetadata:
140-
output.outputConfiguration.sendAdditionalPayMetadata,
141-
allowRetry: output.outputConfiguration.allowRetry,
142-
},
143-
};
144-
default:
145-
return {};
146-
}
147-
});
102+
// TODO: move to controller
103+
this._outputs = outputs.outputs;
148104
}
149105
}
150106

@@ -269,26 +225,6 @@ export class SummaryViewModel {
269225
return details;
270226
}
271227

272-
private getRelevantPages(model: FormModel, state: FormSubmissionState) {
273-
let nextPage = model.startPage;
274-
const relevantPages: any[] = [];
275-
let endPage = null;
276-
277-
while (nextPage != null) {
278-
if (nextPage.hasFormComponents) {
279-
relevantPages.push(nextPage);
280-
} else if (
281-
!nextPage.hasNext &&
282-
!(nextPage instanceof SummaryPageController)
283-
) {
284-
endPage = nextPage;
285-
}
286-
nextPage = nextPage.getNextPage(state, true);
287-
}
288-
289-
return { relevantPages, endPage };
290-
}
291-
292228
get validatedWebhookData() {
293229
const result = webhookSchema.validate(this._webhookData, {
294230
abortEarly: false,
@@ -321,18 +257,6 @@ export class SummaryViewModel {
321257
set outputs(value) {
322258
this._outputs = value;
323259
}
324-
325-
get payApiKey() {
326-
if (isMultipleApiKey(this._payApiKey)) {
327-
return (
328-
this._payApiKey[config.apiEnv] ??
329-
this._payApiKey.test ??
330-
this._payApiKey.production
331-
);
332-
}
333-
return this._payApiKey;
334-
}
335-
336260
/**
337261
* If a declaration is defined, add this to {@link this._webhookData} as a question has answered `true` to
338262
*/

runner/src/server/plugins/engine/models/submission/EmailModel.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import { FormModel } from "server/plugins/engine/models";
2+
import { TEmailModel } from "./types";
23
import config from "server/config";
4+
import { EmailOutputConfiguration } from "@xgovformbuilder/model";
5+
import { WebhookData } from "server/plugins/engine/models/types";
36
const { notifyTemplateId, notifyAPIKey } = config;
47

58
/**
69
* returns an object used for sending email requests. Used by {@link SummaryViewModel}
710
*/
8-
export function EmailModel(model: FormModel, outputConfiguration, webhookData) {
11+
export function EmailModel(
12+
model: FormModel,
13+
outputConfiguration: EmailOutputConfiguration,
14+
webhookData: WebhookData
15+
): TEmailModel {
916
const data: string[] = [];
1017

1118
webhookData?.questions?.forEach((question) => {

runner/src/server/plugins/engine/models/submission/NotifyModel.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,7 @@ import { FormModel } from "server/plugins/engine/models";
22
import { FormSubmissionState } from "server/plugins/engine/types";
33
import { reach } from "hoek";
44
import { NotifyOutputConfiguration, List } from "@xgovformbuilder/model";
5-
6-
export type NotifyModel = Omit<
7-
NotifyOutputConfiguration,
8-
"emailField" | "replyToConfiguration" | "personalisation"
9-
> & {
10-
emailAddress: string;
11-
emailReplyToId?: string;
12-
personalisation: {
13-
[key: string]: string | boolean;
14-
};
15-
};
5+
import { TNotifyModel } from "./types";
166

177
const parseListAsNotifyTemplate = (
188
list: List,
@@ -38,7 +28,7 @@ export function NotifyModel(
3828
model: FormModel,
3929
outputConfiguration: NotifyOutputConfiguration,
4030
state: FormSubmissionState
41-
): NotifyModel {
31+
): TNotifyModel {
4232
const {
4333
addReferencesToPersonalisation,
4434
apiKey,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { FormModel } from "server/plugins/engine/models";
2+
import { FormSubmissionState } from "server/plugins/engine/types";
3+
import {
4+
EmailModel,
5+
WebhookModel,
6+
NotifyModel,
7+
} from "server/plugins/engine/models/submission";
8+
import { WebhookData } from "server/plugins/engine/models/types";
9+
import {
10+
EmailOutputConfiguration,
11+
NotifyOutputConfiguration,
12+
OutputType,
13+
WebhookOutputConfiguration,
14+
} from "@xgovformbuilder/model";
15+
import { OutputData } from "server/plugins/engine/models/submission/types";
16+
17+
export class Outputs {
18+
webhookData: WebhookData;
19+
outputs: (OutputData | unknown)[];
20+
21+
constructor(model: FormModel, state: FormSubmissionState) {
22+
this.webhookData = WebhookModel(model, state);
23+
24+
const outputDefs = model.def.outputs;
25+
this.outputs = outputDefs.map((output) => {
26+
switch (output.type) {
27+
case "notify":
28+
/**
29+
* Typescript does not support nested type discrimination {@link https://github.com/microsoft/TypeScript/issues/18758}
30+
*/
31+
const notifyOutputConfiguration = output.outputConfiguration as NotifyOutputConfiguration;
32+
return {
33+
type: OutputType.Notify,
34+
outputData: NotifyModel(model, notifyOutputConfiguration, state),
35+
};
36+
case "email":
37+
const emailOutputConfiguration = output.outputConfiguration as EmailOutputConfiguration;
38+
return {
39+
type: OutputType.Email,
40+
outputData: EmailModel(
41+
model,
42+
emailOutputConfiguration,
43+
this.webhookData
44+
),
45+
};
46+
case "webhook":
47+
const webhookOutputConfiguration = output.outputConfiguration as WebhookOutputConfiguration;
48+
return {
49+
type: OutputType.Webhook,
50+
outputData: {
51+
url: webhookOutputConfiguration.url,
52+
sendAdditionalPayMetadata:
53+
webhookOutputConfiguration.sendAdditionalPayMetadata,
54+
allowRetry: webhookOutputConfiguration.allowRetry,
55+
},
56+
};
57+
default:
58+
return {} as unknown;
59+
}
60+
});
61+
}
62+
}

0 commit comments

Comments
 (0)