feat(mrf): V4 response migration — PR 2/5 BE V4-native + V3 wire shim#9637
Open
scottheng96 wants to merge 16 commits into
Open
feat(mrf): V4 response migration — PR 2/5 BE V4-native + V3 wire shim#9637scottheng96 wants to merge 16 commits into
scottheng96 wants to merge 16 commits into
Conversation
Adds V4 variants of the BE attachment types (ParsedClearAttachmentAnswerV4, ParsedClearAttachmentFieldResponseV4, ParsedClearFormFieldResponsesV4) and the stripped attachment type (StrippedAttachmentResponseV4) used when extracting attachment binaries before encryption. All additions are purely additive — no existing V3 types are touched. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds isFieldResponseV4Equal (mirrors V3 counterpart, compares answers structurally and md5Hash for attachments), isAttachmentResponseV4 type guard, and createNdiResponsesV4FromRecord for building V4-shaped NDI verified responses. All additions are purely additive. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds constructXxxValidatorV4 for each field type alongside the existing
V3 validators. Key answer-shape changes handled: StringAnswerV4 wraps
value in {value}, RadioAnswerV4 uses {value, isOthersInput}, TableAnswerV4
uses {rowId: {rowNum, value}} keyed object, AddressAnswerV4 uses named
sub-fields, AttachmentAnswerV4 uses value for filename. Also adds
constructFieldResponseValidatorV4 to the factory and validateFieldV4
to the index. All additions are purely additive.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switches the MRF submission flow from V3 to V4 response types throughout: - Receiver types req.body.responses as FieldResponsesV4 from the start - scanAndRetrieveAttachments uses V4 attachment types - validateMultirespondentSubmission compares as V4; upgrades legacy V3 previous submissions (mrfVersion=1) via adaptV3ToV4 before comparison - encryptSubmission removes answerObjectEncryption feature flag check; always encrypts as V4 with mrfVersion=2 - handleNdiResponses uses createNdiResponsesV4FromRecord - Email utilities and validateMrfFieldResponses updated for V4 answer shapes - All multirespondent-submission.service.ts signatures use FieldResponsesV4 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The reference V4 wiring commit predated these helpers being added to develop. They were dropped by the cherry-pick conflict resolution but are still called from multirespondent-submission.service.ts. Reintroduce both, but rewrite buildMrfResponseJson as a thin V4 wrapper over getQuestionAnswerPairsForMultipleFields. The `delimiter` param is accepted for caller compatibility but is currently unused — table-cell separator is hardcoded post-V4 migration. Restoring admin metadata.delimiter customisation in the email JSON is a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Under PR2 the entire MRF pipeline assumes V4-shaped responses, but stale FE clients (cached SPA bundles, open browser tabs) may still POST V3 shape after the BE has been deployed. Without a shim, every such submission would fail silently: V4 validators reject string answers (no `.value`), and attachment injection writes filename/content into V4 paths the V3 answer doesn't have. Detect V3 MRF via body.version (>=3 && <4), run the SDK's adaptV3ToV4 on the responses in place, then bump body.version to 4 so downstream code uses the V4 path uniformly. Runs before addAttachmentToResponses so attachment buffers land in V4-shaped answers. Question text is left empty on adapted responses since the form definition isn't loaded yet at this stage. Downstream code that needs the question text should source it from the form definition. A WARN log is emitted on each adapted submission for monitoring stale-FE traffic during the rollout window. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related UX/compatibility tweaks on top of the V4 wiring: 1. Forms with a webhook URL still encrypt as V3 (mrfVersion: 1) so existing webhook consumers, which parse the encrypted payload as V3-shaped, keep working unchanged. Convert V4 -> V3 via the SDK's adaptV4ToV3 right before passing to cryptoV3.encrypt. In-process state (encryptedPayload.responses, email Q/A, NDI merging) stays V4 throughout — only the encrypted blob differs. 2. Restore the "[Verified]" prefix on email/mobile question titles in the Q/A pairs used for email body / PDF rendering — this UX hint was dropped in the V4 wiring commit. Toggle via the new includeVerifiedPrefix option (default true). buildMrfResponseJson passes false: the JSON dump is for machine consumers and carries raw question text without the prefix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fe16771 to
b1df74b
Compare
3a327b2 to
f14c57b
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
This PR flips the backend Multi-Respondent Form (MRF) submission pipeline to be V4-native internally (types/validation/Q&A helpers/encryption flow), while preserving compatibility for (a) stale FE clients still sending V3 and (b) existing webhook consumers that expect V3-encrypted payloads.
Changes:
- Migrates MRF request/processing types and field validators to operate on V4 response shapes end-to-end, including attachment scanning and Q/A extraction utilities.
- Adds a receiver-side V3 → V4 shim for
version >= 3 && < 4submissions so stale FE clients continue working. - Preserves webhook compatibility by encrypting webhook forms as V3 (via V4 → V3 adaptation at encryption time) while keeping in-process handling in V4.
Reviewed changes
Copilot reviewed 45 out of 45 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/shared/package.json | Adds CJS require export for ./constants/form to keep require('formsg-shared/constants/form') working. |
| packages/sdk/cjs-entry.cjs | Exports callable default with named exports attached for CJS consumers. |
| apps/backend/src/types/api/submission.ts | Introduces V4 parsed/clear response types, including receiver-enriched V4 attachment answers. |
| apps/backend/src/types/api/multirespondent_submission.ts | Switches MRF API body and DTO response types from V3 to V4. |
| apps/backend/src/app/utils/response-v4.ts | Adds V4 response equality helper (used in non-editable field checks). |
| apps/backend/src/app/utils/field-validation/validators/yesNoValidator.ts | Adds Yes/No validator for V4 response shape. |
| apps/backend/src/app/utils/field-validation/validators/uenValidator.ts | Adds UEN validator for V4 response shape. |
| apps/backend/src/app/utils/field-validation/validators/textValidator.ts | Adds short/long text validators for V4 response shape. |
| apps/backend/src/app/utils/field-validation/validators/tableValidator.ts | Adds table validator for V4 keyed-row table answer shape. |
| apps/backend/src/app/utils/field-validation/validators/signatureValidator.ts | Adds signature validator for V4 signature answer shape. |
| apps/backend/src/app/utils/field-validation/validators/sectionValidator.ts | Adds section validator for V4 response shape. |
| apps/backend/src/app/utils/field-validation/validators/ratingValidator.ts | Adds rating validator for V4 response shape. |
| apps/backend/src/app/utils/field-validation/validators/radioButtonValidator.ts | Adds radio validator for V4 { value, isOthersInput } answer shape. |
| apps/backend/src/app/utils/field-validation/validators/numberValidator.ts | Adds number validator for V4 response shape. |
| apps/backend/src/app/utils/field-validation/validators/nricValidator.ts | Adds NRIC validator for V4 response shape. |
| apps/backend/src/app/utils/field-validation/validators/mobileNoValidator.ts | Adds mobile validator for V4 verifiable answer shape and V4 signature validation hook. |
| apps/backend/src/app/utils/field-validation/validators/homeNoValidator.ts | Adds home number validator for V4 response shape. |
| apps/backend/src/app/utils/field-validation/validators/emailValidator.ts | Adds email validator for V4 verifiable answer shape and V4 signature validation hook. |
| apps/backend/src/app/utils/field-validation/validators/dropdownValidator.ts | Adds dropdown validator for V4 response shape. |
| apps/backend/src/app/utils/field-validation/validators/decimalValidator.ts | Adds decimal validator for V4 response shape. |
| apps/backend/src/app/utils/field-validation/validators/dateValidator.ts | Adds date validator for V4 response shape. |
| apps/backend/src/app/utils/field-validation/validators/countryRegionValidator.ts | Adds country/region validator for V4 response shape. |
| apps/backend/src/app/utils/field-validation/validators/common.ts | Adds V4 signature validator constructor for verifiable fields. |
| apps/backend/src/app/utils/field-validation/validators/childrenValidator.ts | Adds V4 “always pass” children validator (MRF not using children fields). |
| apps/backend/src/app/utils/field-validation/validators/checkboxValidator.ts | Adds checkbox validator for V4 checkbox answer shape. |
| apps/backend/src/app/utils/field-validation/validators/attachmentValidator.ts | Adds V4 attachment validator for nested answer.content/filename. |
| apps/backend/src/app/utils/field-validation/validators/addressValidator.ts | Adds address validators for V4 address subfield answer shape. |
| apps/backend/src/app/utils/field-validation/index.ts | Adds validateFieldV4 path and V4 validation error mapping. |
| apps/backend/src/app/utils/field-validation/answerValidator.factory.ts | Adds V4 field-response validator factory wiring. |
| apps/backend/src/app/modules/submission/submission.utils.ts | Adds V4 attachment type guard used by the MRF pipeline. |
| apps/backend/src/app/modules/submission/submission.service.ts | Adds GuardDuty scan/download helper for V4 attachment response shape. |
| apps/backend/src/app/modules/submission/submission.errors.ts | Adds ValidateFieldErrorV4 error class. |
| apps/backend/src/app/modules/submission/receiver/receiver.utils.ts | Adds receiver-side V3→V4 MRF shim and updates attachment injection for V4 responses. |
| apps/backend/src/app/modules/submission/receiver/receiver.types.ts | Updates receiver body type-guard for v3+ submissions to V4 response record. |
| apps/backend/src/app/modules/submission/receiver/receiver.service.ts | Plumbs receiver-side shim into multipart parsing flow and updates typings. |
| apps/backend/src/app/modules/submission/receiver/receiver.middleware.ts | Updates multirespondent receiver middleware request body type to V4. |
| apps/backend/src/app/modules/submission/multirespondent-submission/multirespondent-submission.utils.ts | Migrates MRF utilities to V4 response shapes (validation, Q/A extraction, response JSON). |
| apps/backend/src/app/modules/submission/multirespondent-submission/multirespondent-submission.types.ts | Updates MRF types to V4 response types and V4 stripped attachment type. |
| apps/backend/src/app/modules/submission/multirespondent-submission/multirespondent-submission.service.ts | Migrates service-layer typing to V4 responses (e.g. approval checks and email flows). |
| apps/backend/src/app/modules/submission/multirespondent-submission/multirespondent-submission.middleware.ts | Main MRF pipeline flip to V4, including webhook V3 encryption branch and V4 equality comparison. |
| apps/backend/src/app/modules/submission/multirespondent-submission/tests/multirespondent-submission.utils.spec.ts | Updates unit tests for V4 response shapes and new Q/A formatting behavior. |
| apps/backend/src/app/modules/submission/multirespondent-submission/tests/multirespondent-submission.service.spec.ts | Updates service tests to V4 { value } answers for yes/no fields in fixtures. |
| apps/backend/src/app/modules/submission/multirespondent-submission/tests/multirespondent-submission.middleware.spec.ts | Updates middleware tests for webhook V3 encryption branch and V3→V4 previous-response adaptation path. |
| apps/backend/src/app/modules/spcp/spcp.util.ts | Adds V4 NDI response builder for inclusion in V4 email/encrypted payload handling. |
| apps/backend/src/app/modules/core/core.errors.ts | Adds new error code for V4 field validation failures. |
Comment on lines
+84
to
+87
| body.responses = adaptV3ToV4( | ||
| body.responses as unknown as FormFieldsV3, | ||
| ) as FieldResponsesV4 | ||
| } |
Comment on lines
+441
to
+443
| if (checkboxAnswer.othersInput) { | ||
| selectedAnswers.push(checkboxAnswer.othersInput) | ||
| } |
Comment on lines
+585
to
+589
| const addressAnswer = response.answer as Record<string, { value: string }> | ||
| for (const subField of Object.keys(addressAnswer)) { | ||
| entries.push({ | ||
| question: `${field.title} - ${subField}`, | ||
| answer: addressAnswer[subField]?.value ?? '', |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
PR 1 added the V4 building blocks (types, utils, validators) as pure additions. This PR is the internal pipeline flip — the BE stops treating V3 as the working format and instead handles every MRF submission as V4 end-to-end, with the wire-side V3 shape adapted into V4 at the receiver.
Two compatibility constraints to preserve:
mrfVersion: 1). They cannot break.Solution
Four commits on top of
feat/mrf-v4-pr1-foundation:feat(mrf): migrate submission pipeline to V4 response types end-to-end— cherry-pick of the original V4 wiring commit fromfeat/mrf-v4-responses. Receiver typesreq.body.responsesasFieldResponsesV4;scanAndRetrieveAttachments,validateMultirespondentSubmission,encryptSubmission,handleNdiResponses, email utils, andmultirespondent-submission.service.tsall consume V4 throughout.mrfVersiondefaults to2. Legacy in-flight chains withmrfVersion: 1in DB still complete via the existingadaptV3ToV4call invalidateMultirespondentSubmission(unchanged).fix(mrf): restore buildMrfResponseJson and getFormDelimiter for V4— these helpers were added todevelopafter the reference branch forked. The cherry-pick dropped them; the service still calls them. Restored as V4-native (the rewrite delegates togetQuestionAnswerPairsForMultipleFields). Note: thedelimiterparam is accepted for caller compatibility but is currently a no-op — table-cell separator is hardcoded to'; 'in the V4 utils. Restoring adminmetadata.delimitercustomisation is a follow-up.feat(mrf): adapt stale V3 MRF submissions to V4 at the receiver— V3→V4 shim at the receiver boundary. Triggered bybody.version >= 3 && < 4, runs the SDK'sadaptV3ToV4in place, then bumpsbody.versionto 4 so downstream code can uniformly treat the body as V4. Runs beforeaddAttachmentToResponsesso attachment buffers land in V4-shaped answers. Emits a WARN log on each adapted submission for stale-FE monitoring during rollout. Adapted responses havequestion: ''(form definition not loaded at receiver layer); the existing downstream fallbackresponse.question || formField.titlehandles this cleanly.feat(mrf): webhook V3 encryption path + restore [Verified] Q/A prefix— two related compatibility tweaks:formDef.webhook?.urlset encrypt as V3 (mrfVersion: 1) viaadaptV4ToV3right beforecryptoV3.encrypt. In-process state (encryptedPayload.responses, email Q/A, NDI merging) stays V4 throughout — only the encrypted blob differs. Webhook consumers continue to parse V3 unchanged.[Verified]prefix on email/mobile question titles in Q/A pairs used for email body / PDF rendering — this UX hint was dropped by the wiring commit. Toggle via the newincludeVerifiedPrefixoption (defaulttrue).buildMrfResponseJsonpassesfalseso the JSON dump carries raw question text.Status: Being soaked on a staging environment in parallel with this review. Marked as draft until staging confirms parity with V3 production behavior.
Migration roadmap (5 PRs):
mrfVersion: 1) via V4→V3 adapter at encryption time only — preserves the V3 webhook payload contract.createResponsesV3with a V4-thin wire builder (FE sendsfieldType + answeronly; BE enriches metadata). FE bumps submissionversionto 4.Risk worth flagging. The original 5-PR plan had an intermediate step where the BE accepted V4 from wire but routed through the V3 pipeline (adapt V4→V3 at entry). That gave the V4 utilities first exercise on a non-critical path before the internal flip. By skipping it, the V4 validators / Q/A helpers / encryption branch get their first prod exercise on every MRF submission the moment this PR lands. Staging soak is the primary mitigation; rolling back is just reverting the merge.
Breaking Changes
mrfVersion: 1) whenformDef.webhook?.urlis set.mrfVersion: 2is new on the DB record for non-webhook forms.Tests
TC1: V4-shaped FE submission (the happy path PR 3 will enable)
body.version: 4and V4-shaped responsesmrfVersionis2TC2: Stale-FE V3 submission (receiver shim)
body.version: 3and V3-shaped responses (stringanswer, etc.)"Adapting V3 MRF submission to V4 — client is on a stale build and should refresh"mrfVersionis2(non-webhook) or1(webhook)TC3: Webhook form encryption fallback
mrfVersionis1encryptedContentand verify the shape is V3 (answeras string for text,addressSubFieldsfor address, etc.)TC4:
[Verified]prefix on signed email/mobilesignatureon the answer)[Verified] <field title>for that fieldresponseJson) does not include the[Verified]prefix — raw title onlyTC5: Multi-step continuity with attachments
TC6: Legacy in-flight chain (
mrfVersion: 1in DB)mrfVersion: 1(pre-migration)validateMultirespondentSubmissioncallsadaptV3ToV4on the previous-step responses before comparison (no field comparison failures)TC7: Build & static checks
pnpm --filter formsg-backend build— verified locally: clean)