Skip to content

Commit c9e0c26

Browse files
committed
chore: move function to mrf utils
1 parent 1bf1b5d commit c9e0c26

File tree

3 files changed

+241
-246
lines changed

3 files changed

+241
-246
lines changed

src/app/modules/submission/multirespondent-submission/multirespondent-submission.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ import {
5757
} from '../submission.errors'
5858
import { uploadAttachments } from '../submission.service'
5959
import { AttachmentMetadata } from '../submission.types'
60+
import { getMrfSubmissionWorkflowStatus } from '../submission.utils'
6061
import {
61-
getMrfSubmissionWorkflowStatus,
6262
getPdfResponsesData,
6363
getQuestionAnswerPairsForMultipleFields,
64-
} from '../submission.utils'
64+
} from './multirespondent-submission.utils'
6565
import { reportSubmissionResponseTime } from '../submissions.statsd-client'
6666

6767
import { MultirespondentSubmissionContent } from './multirespondent-submission.types'

src/app/modules/submission/multirespondent-submission/multirespondent-submission.utils.ts

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@ import { err, ok, Result } from 'neverthrow'
44
import {
55
BasicField,
66
FieldResponsesV3,
7+
FieldResponseV3,
78
FormFieldDto,
89
FormWorkflowStepDto,
910
MultirespondentSubmissionDto,
11+
NdiResponseV3,
1012
PublicMultirespondentSubmissionDto,
1113
SubmissionType,
1214
WorkflowType,
1315
} from '../../../../../shared/types'
1416
import { stripDropdownFieldOptionsToRecipientsMap } from '../../../../../shared/utils/strip-dropdown-field-optionsToRecipientsMap'
1517
import { stripWorkflowEmails } from '../../../../../shared/utils/strip-workflow-emails'
1618
import {
19+
EmailRespondentConfirmationField,
1720
FormFieldSchema,
1821
MultirespondentSubmissionData,
1922
} from '../../../../types'
@@ -27,6 +30,11 @@ import {
2730
ValidateFieldErrorV3,
2831
} from '../submission.errors'
2932
import { buildMrfMetadata } from '../submission.utils'
33+
import { startsWithSPCPFieldTitle } from '../../spcp/spcp.util'
34+
import { convertToSignaturePngDataUri } from '../../../utils/convert-vector-array-to-png'
35+
import { SIGNATURE_CAPTURED_STRING } from '../../../../../shared/utils/signature'
36+
import { handleAddressResponseDisplay } from '../../../../../shared/utils/address'
37+
import { CLIENT_CHECKBOX_OTHERS_INPUT_VALUE } from '../../../../../shared/constants'
3038

3139
/**
3240
* Creates and returns a MultirespondentSubmissionDto object from submissionData and
@@ -269,3 +277,228 @@ export const extractRespondentCopyEmailDatas = ({
269277
return [] // no respondent copy emails found
270278
})
271279
}
280+
281+
export type QuestionAnswerPair = {
282+
question: string
283+
answer: string
284+
signatureDataPngDataUri?: string
285+
}
286+
287+
/**
288+
* Given a single form field and its response, extracts question-answer pairs.
289+
* Used for email body/pdf outputs and individualResponsePage displays
290+
* Returns an array since some fields (e.g. table, children) will have
291+
* multiple question-answer pairs per response
292+
* @param formField - Single form field schema. Does not include Ndi responses, @see getQuestionAnswerPairsForMultipleFields on how to include ndi responses.
293+
* @param response - Response for the given form field
294+
* @returns An array of QuestionAnswer objects representing the extracted question-answer pairs for the given form field.
295+
*/
296+
const getQuestionAnswerPairsForOneField = ({
297+
formField,
298+
response,
299+
includeSignatureDataPngDataUri,
300+
}: {
301+
formField: FormFieldSchema | FormFieldDto
302+
response: FieldResponseV3
303+
includeSignatureDataPngDataUri: boolean
304+
}): QuestionAnswerPair[] => {
305+
let questionTitle = formField.title
306+
let answer = ''
307+
let answerArray: string[] = []
308+
const questionAnswerPairs: QuestionAnswerPair[] = []
309+
310+
switch (response.fieldType) {
311+
case BasicField.Attachment:
312+
questionTitle = `[Attachment] ${questionTitle}`
313+
answer = response.answer.answer
314+
break
315+
case BasicField.Address: {
316+
const {
317+
postalCode,
318+
blockNumber,
319+
streetName,
320+
buildingName,
321+
levelNumber,
322+
unitNumber,
323+
} = response.answer.addressSubFields
324+
answerArray = [
325+
blockNumber,
326+
streetName,
327+
buildingName,
328+
levelNumber,
329+
unitNumber,
330+
postalCode,
331+
] // move postal code to end of array
332+
answer = handleAddressResponseDisplay(Object.values(answerArray)).join(
333+
', ',
334+
)
335+
break
336+
}
337+
case BasicField.Email:
338+
case BasicField.Mobile:
339+
answer = response.answer.value
340+
break
341+
case BasicField.Table:
342+
if (formField.fieldType !== BasicField.Table || !response.answer) break
343+
// eslint-disable-next-line no-case-declarations
344+
const idToColTitleMap = formField.columns.reduce(
345+
(acc, col) => {
346+
acc[col._id] = col.title
347+
return acc
348+
},
349+
{} as Record<string, string>,
350+
)
351+
352+
for (const row of response.answer) {
353+
const validColumns = Object.entries(row).filter(
354+
([colId]) => colId in idToColTitleMap,
355+
)
356+
357+
const delimitedColumnTitles = validColumns
358+
.map(([colId]) => {
359+
const colTitle = idToColTitleMap[colId]
360+
return `${colTitle}`
361+
})
362+
.join('; ')
363+
364+
const delimitedColumnAnswers = validColumns
365+
.map(([, colAns]) => colAns ?? '')
366+
.join('; ')
367+
368+
const question = `[Table] ${formField.title} (${delimitedColumnTitles})`
369+
const answer = delimitedColumnAnswers
370+
371+
questionAnswerPairs.push({
372+
question,
373+
answer,
374+
})
375+
}
376+
return questionAnswerPairs
377+
case BasicField.Radio:
378+
answer =
379+
'value' in response.answer
380+
? response.answer.value
381+
: 'othersInput' in response.answer
382+
? response.answer.othersInput
383+
: ''
384+
break
385+
case BasicField.Checkbox:
386+
// eslint-disable-next-line no-case-declarations
387+
const selectedAnswers =
388+
(response.answer.othersInput
389+
? [...response.answer.value, response.answer.othersInput]
390+
: [...response.answer.value]
391+
).filter((val) => val !== CLIENT_CHECKBOX_OTHERS_INPUT_VALUE) ?? []
392+
393+
answer = selectedAnswers.toString()
394+
break
395+
case BasicField.Signature: {
396+
const signatureQuestionAnswer = {
397+
question: `[signature] ${questionTitle}`,
398+
answer: SIGNATURE_CAPTURED_STRING,
399+
signatureDataPngDataUri: includeSignatureDataPngDataUri
400+
? convertToSignaturePngDataUri(response.answer.value)
401+
: undefined,
402+
}
403+
return [signatureQuestionAnswer]
404+
}
405+
case BasicField.Children:
406+
if (!response.answer.childFields || !response.answer.child) {
407+
break
408+
}
409+
for (const [index, child] of response.answer.child.entries()) {
410+
questionAnswerPairs.push({
411+
question: `Child ${index + 1}: ${response.answer.childFields.toString()}`,
412+
answer: child
413+
? child.toString()
414+
: response.answer.childFields.map(() => '').toString(),
415+
})
416+
}
417+
return questionAnswerPairs
418+
default:
419+
answer = response.answer
420+
}
421+
422+
questionAnswerPairs.push({
423+
question: questionTitle,
424+
answer,
425+
})
426+
return questionAnswerPairs
427+
}
428+
429+
/**
430+
* Given multiple form fields and their responses, extracts question-answer pairs.
431+
* @param formFields - List of form fields schemas
432+
* @param responses - Corresponding list of responses to the given form fields
433+
* @returns An array of QuestionAnswer pairs representing the extracted question-answer pairs for the all the given form fields.
434+
*/
435+
export const getQuestionAnswerPairsForMultipleFields = ({
436+
formFields,
437+
responses,
438+
includeSignatureDataPngDataUri = false,
439+
}: {
440+
formFields: FormFieldSchema[] | FormFieldDto[]
441+
responses: FieldResponsesV3
442+
includeSignatureDataPngDataUri?: boolean
443+
}): QuestionAnswerPair[] => {
444+
const questionAnswerPairs: QuestionAnswerPair[] = []
445+
if (!formFields || !responses) {
446+
return []
447+
}
448+
for (const currentFormField of formFields) {
449+
const questionTitle = currentFormField.title
450+
const response = responses[currentFormField._id]
451+
452+
if (!response || !questionTitle) continue
453+
const questionAnswerPairsForCurrentFormField =
454+
getQuestionAnswerPairsForOneField({
455+
formField: currentFormField,
456+
response,
457+
includeSignatureDataPngDataUri,
458+
})
459+
460+
questionAnswerPairs.push(...questionAnswerPairsForCurrentFormField)
461+
}
462+
463+
// Add Ndi responses if they exist
464+
for (const key in responses) {
465+
if (startsWithSPCPFieldTitle(key)) {
466+
const value = responses[key] as NdiResponseV3
467+
questionAnswerPairs.push({
468+
question: key,
469+
answer: value.answer,
470+
})
471+
}
472+
}
473+
return questionAnswerPairs
474+
}
475+
476+
/**
477+
* Prepares responses data from MRF responses to PDF html format
478+
* @param formFields - The form fields schema
479+
* @param responses - The mrf responses to the form fields
480+
* @returns list of EmailRespondentConfirmationField used for email & pdf generation
481+
*/
482+
export const getPdfResponsesData = ({
483+
formFields,
484+
responses,
485+
}: {
486+
formFields: FormFieldSchema[] | FormFieldDto[]
487+
responses: FieldResponsesV3
488+
}): EmailRespondentConfirmationField[] => {
489+
if (!formFields || !responses) return []
490+
491+
const questionAnswerPairs = getQuestionAnswerPairsForMultipleFields({
492+
formFields,
493+
responses,
494+
includeSignatureDataPngDataUri: true,
495+
})
496+
497+
return questionAnswerPairs.map((questionAnswerPair) => {
498+
return {
499+
question: questionAnswerPair.question,
500+
answerTemplate: [questionAnswerPair.answer],
501+
answer: questionAnswerPair.signatureDataPngDataUri,
502+
}
503+
})
504+
}

0 commit comments

Comments
 (0)