fix(kyc): stop KycStep.update from clearing omitted fields - #4540
Open
joshuakrueger-dfx wants to merge 1 commit into
Open
fix(kyc): stop KycStep.update from clearing omitted fields#4540joshuakrueger-dfx wants to merge 1 commit into
joshuakrueger-dfx wants to merge 1 commit into
Conversation
update() built its payload from the raw parameters, so Object.assign wrote undefined onto the entity for every argument a caller left out. Callers use undefined to mean "leave unchanged", so two paths silently lost state: - updateFinancialData calls update(undefined, responses). The step lost its status, and KycStepMapper.toStepBase then mapped StepMap[undefined] to undefined. An incomplete draft submit returned HTTP 200 without status and without sequenceNumber, although KycStepBase declares both as required. - mergeUserData passes status undefined for every slave step that is not in the cancel list, COMPLETED included. The check below it - slave.kycSteps.some(k => (VIDEO || SUMSUB_VIDEO) && k.isCompleted) - was therefore never true, so identificationType = VIDEO_ID and bankTransactionVerification = UNNECESSARY were never applied on a merge. That rule was added in #1424 and has been inert since 6fb9d18 moved the loop onto update(). The database payload is unchanged for every caller: TypeORM already drops undefined values from the update statement, so the defect was in memory only. status becomes optional, matching the two call sites that pass undefined, and addComment returns string because Array.prototype.join always does.
joshuakrueger-dfx
requested review from
TaprootFreak and
davidleomay
as code owners
July 31, 2026 13:20
This was referenced Jul 31, 2026
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.
Why
KycStep.update()built its payload straight from its parameters:Object.assigncopiesundefined, and both callers that omit an argument useundefinedto meanleave this field alone. So the entity lost the field in memory. Two consequences, both live:
1.
PUT /kyc/data/financial/:idreturned a response missing two required fields.updateFinancialDatacallskycStep.update(undefined, data.responses). The step lost itsstatus,and
KycStepMapper.toStepBasemapsStepMap[undefined]→undefined, which drops out of the JSON.An incomplete draft submit therefore answered
200withoutstatusand withoutsequenceNumber,although
KycStepBasedeclares both as@ApiProperty(required).What this does not fix. It is not the cause of the "status does not change after submitting the
financial information" report. Both frontends evaluate the submit response through
isStepDone(result)in@dfx.swiss/core, which checks[IN_REVIEW, ON_HOLD, COMPLETED].includes(result.status). That wasfalsewith the field absent andis
falsewithinProgresspresent, so the user-visible behaviour is unchanged. That report isstill open and is being diagnosed separately; this PR is the contract violation and the merge branch
below, nothing more.
2. An AML branch in
mergeUserDatahas been dead since 2024-11-11.The cancel loop passes
status: undefinedfor every slave step whose status is not in the cancellist —
COMPLETEDincluded. Because the status was then wiped, the check further down the samemethod could never be true:
That rule was added deliberately in #1424 (2024-07-09) and worked;
6fb9d18536(
[NOTASK] fix userData merge, 2024-11-11) moved the loop from direct assignment ontoupdate()and disabled it as a side effect. There is no revert, no ticket and no comment anywhere that says
this was wanted. This PR makes that branch fire again, which means a merge can now set
bankTransactionVerification = UNNECESSARYwhere it previously did not. That is the intendedbehaviour, but it is a change in an AML-relevant outcome and it is the reason this fix is its own PR
rather than a line in a larger one — please look at it explicitly.
What changed
update()drops entries whose value isundefinedbefore assigning and before returning the partial.statusbecomes optional in the signature, matching the two call sites that passundefined.It only compiled before because the project has no
strictNullChecks.addCommentreturnsstringinstead ofstring | undefined—Array.prototype.joinalwaysreturns a string, and
update()in the same file is its only caller.Blast radius
update()has 11 production call sites. The change is in-memory only: TypeORM already stripsundefinedfrom the update statement (UpdateQueryBuilder), so the SQL sent for every caller isbyte-for-byte what it was. Of the 11, two pass
undefined:kyc.service.tsupdateFinancialDatastatuswiped → response loststatus/sequenceNumberuser-data.service.tsmergeUserDataupdateFileData,updateKycStepAndLogsequenceNumberwiped → absent from the responsesequenceNumberwiped in memory, never read afterwardsNo caller passes
undefinedin order to clear a field; clearing is done with explicitnullinthis repo (e.g.
reminderSentDate: nullinpause()).How much damage the dead branch actually did — measured, not assumed
The fix is forward-only, so the question is whether the accounts merged while the branch was inert
need a backfill. Measured against production via the read-only
/gs/debugquery endpoint:Video/SumsubVideoident stepidentificationType = VideoIdOnlineId15,null11,Manual1)So the upper bound for accounts this regression could have affected is two (
userData191940and 284664), and it is genuinely an upper bound: the step's
updatedtimestamp is not the mergedate, and a mismatch has other possible causes — an account that completed an online ident before
the video one legitimately carries
OnlineId.No backfill is proposed. Two possible cases do not justify a migration over
user_data; if thetwo turn out to be real, they are a manual correction. The number is recorded here so the decision
is visible rather than implied.
(Method, reproducible:
kyc_stepwherename = Ident,type IN (Video, SumsubVideo),status = Completed, grouped byuserDataId→ 791 ids; thenuser_datafor those ids in batches of100, which is the endpoint's
INcap. A first attempt used batches of 200, was rejected withIN list size must be 1..100, and the rejected batches parsed as zero rows — the numbers above arefrom the corrected run, with all 8 batches confirmed to have returned 791 rows and no error object.)
Tests
Six new tests. Every one was checked twice — once with the fix removed, once with the filter
mutated — so that "removing the fragment turns it red" is not the only evidence:
kyc-step.entity.tsreset todevelop)v !== undefined→v !== nullThe five are: the entity omit case, the financial draft response, the
updateFileDatasequenceNumber, and both merge cases (VideoandSumsubVideo). The sixth test — an explicitstatus change still being applied — stays green under both probes on purpose: it is the
counter-direction guard that catches a filter that is too broad, not a proof of the fix.
Full gate on the exact revision
3a45ada4f:lint(empty output),format:check,type-checkand
buildall exit 0;Test Suites: 340 passed, 340 of 347 total,Tests: 6211 passed, 174 skipped, 6385 total; coverage gate exit 0.Scope
Deliberately not in this PR: the
complete/missingFieldssubmit response, any change toFinancialService, and theconditionscontract declaration (that one is a separate PR). Thisbranch touches
kyc-step.entity.tsand four spec files, nothing else.