fix(kyc): keep status and sequence number on the financial data response - #4669
fix(kyc): keep status and sequence number on the financial data response#4669joshuakrueger-dfx wants to merge 3 commits into
Conversation
|
EN: Working on this now — job |
marassteiner
left a comment
There was a problem hiding this comment.
Verdict: Approve
No merge blockers. The PR correctly restores required status / sequenceNumber fields on KYC step responses that were wiped in memory by KycStep.update() + Object.assign when optional args were omitted.
Summary
- Financial incomplete path:
update(undefined, responses)→inProgress(responses)— does not putsequenceNumberinto the partial, keepsIN_PROGRESSstatus (already guaranteed bygetPendingStepOrThrow). - Complete financial path: subsequent
internalReview()also leavessequenceNumberintact; response mapper still sees both fields. - Other three response-producing call sites (
updateFileData,updateKycStepAndLog,reviewNationalityData): passkycStep.sequenceNumberas 4th arg (evaluated beforeObject.assignwipe). - Deliberately does not change
KycStep.update()itself — avoids merge-path /checkDfxApprovalside effects (#4540 territory). Scope is minimal and appropriate. - Four new unit tests pin concrete
sequenceNumber(and status for financial) on the mapped response, not just the entity.
Merge blockers
None.
Follow-up findings (non-blocking)
Test-only hygiene from the conformity pass — tracked in #4893:
- Import grouping for
AccountType/KycStepStatus(kyc.service.spec.ts:24-25) as anyonuploadUserFilemock (kyc.service.spec.ts:908)as anyon nationality DTO fragment (kyc.service.spec.ts:1016-1017)
Optional coverage gaps (not defects): complete-financial response assert; nationality branches other than residence-permit; status asserts on non-financial paths.
Local verification
| Check | Result |
|---|---|
| CI on PR (Build and checks, Test shards, Coverage, CodeQL, review) | All SUCCESS |
npm ci |
OK |
npm run build |
exit 0 |
npm test -- --testPathPattern=kyc/services/__tests__/kyc.service.spec |
63/63 passed |
| ESLint on the two changed files | clean |
Full API boot (node dist/src/main) |
skipped — environmental |
Local boot (Case 2 — outside this diff): process exits before Nest init with:
Error: Missing REALUNIT_W2W_GAS_LOW_BALANCE_THRESHOLD
at dist/src/config/config.js (Configuration constructor)
Evidence this is not the PR:
- Diff only touches
kyc.service.ts+ its unit test file - Failure is a pre-existing fail-loud config guard for RealUnit W2W gas (
src/config/config.tsaround theREALUNIT_W2W_GAS_LOW_BALANCE_THRESHOLDread) - Review host has no full
.env/ secrets; the PR neither adds nor uses that variable
LOCAL_RUN_SKIPPED_ENV for full process boot; unit suite + build exercised the change successfully.
Recommendation
Approve and merge when ready. No request-changes items.
TaprootFreak
left a comment
There was a problem hiding this comment.
EN: Changes required: kyc.service.ts is not at 100% on the whole file and is not pinned in the coverage ratchet.
DE: Änderung zwingend erforderlich: kyc.service.ts ist nicht auf der ganzen Datei bei 100% und nicht im Coverage-Ratchet gepinnt.
Details
CONTRIBUTING.md § Test Coverage: every file a PR touches must reach 100% on all four metrics (branches, functions, lines, statements) on the whole file, and must be pinned in jest.coverage-gate.config.js in the same PR.
This PR touches src/subdomains/generic/kyc/services/kyc.service.ts and adds specs only for the changed response-field paths (updateFinancialData, updateFileData, updateKycStepAndLog, updateNationalityStep).
Gaps:
jest.coverage-gate.config.jsis not in the PR.kyc.service.tsis not pinned on this head.- New tests for four methods do not bring this large existing service to 100%. Touching it makes the entire file the author's obligation. If that is not feasible here, cover it in a preparatory PR first, then land this fix.
E2E/handbook: not applicable (no new HTTP surface, no handbook artefacts). The unit/ratchet gap is sufficient to block.
Required:
- Bring
kyc.service.tsto 100% on all four metrics (measure with the coverage-gate compile settings; seedocs/coverage-gate.md). - Add the path to
PINNED_LOGICinjest.coverage-gate.config.jsin this PR. - State the per-file numbers in the PR description.
updateFinancialData passed undefined for status into KycStep.update, which Object.assign copies onto the entity. KycStepMapper.toStepBase then dropped both status and sequenceNumber from the response, although KycStepBase declares them required. Use inProgress(), the pattern the other result writes on a running step already use. The step is guaranteed to be in progress here, because getPendingStepOrThrow throws otherwise.
updateFileData, updateKycStepAndLog and reviewNationalityData omitted the sequenceNumber argument of KycStep.update, so Object.assign copied undefined onto the entity and KycStepMapper.toStepBase dropped the field from the response, although KycStepBase declares it required. Pass the current value through at each call site instead of changing update() itself: the method has 11 production call sites, several of them on the merge path, where the same change would alter in-memory behaviour.
CONTRIBUTING requires every touched file at 100% on all four metrics and pinned in the same PR. The unreachable nationality-missing branch in completeIdent is dropped because the outer guard already requires nationality. The second includesSameName call is dropped because the helper is symmetric on word overlap.
c0b4b07 to
73238d0
Compare
Not symptom-driven: the "status does not change after submitting the financial information"
report is unresolved — the raw response body was requested and never received. This PR does not
claim to close it. What is proven is a contract break found while tracing that path.
Scale: deterministic on every call, across 13 controller routes —
sequenceNumberwasmissing from every KycStep response built after a
KycStep.update()call that omitted it, andstatusadditionally from the financial-data one. How many clients trip over it is not measuredand not claimed.
Smaller fix considered: this PR is the smallest fix. The larger one (#4540, now draft) repairs
KycStep.update()generically and is not merged for the reason under Why not one change insideupdate().Problem
PUT /kyc/data/financial/:idanswers200with a body that has nostatusand nosequenceNumber, althoughKycStepBasedeclares both required — the endpoint violates its ownSwagger contract.
updateFinancialDatacallskycStep.update(undefined, data.responses).KycStep.update()builds{ status, result, comment, sequenceNumber }and runsObject.assign(this, update), which copiesundefinedonto the entity.KycStepMapper.toStepBasethen mapsStepMap[undefined]→undefined, and both fields drop out of the JSON. The DB row is unaffected — TypeORM stripsundefinedbefore theSETclause — the loss is in memory, on the object that becomes the response.For our own frontends the visible behaviour is the same either way: both go through
isStepDone(result)in@dfx.swiss/core, which isfalsewith the field absent andfalsewithinProgresspresent. A third-party client generated from the contract is a different matter.The same wipe hits three further call sites, so
sequenceNumberwas missing from 13 routes intotal — the table below lists them.
Solution
KycStep.update()is the only entity method that putssequenceNumberinto the partial, so it isthe only one that can wipe it. Four call sites omitted the argument and mapped the result into a
response; each now passes the current value through. The financial-data one additionally lost
statusand is fixed differently, because there the status was omitted too:kyc.service.ts:963updateFinancialDatakycStep.inProgress(data.responses)PUT /kyc/data/financial/:id)kyc.service.ts:820updateFileDatakycStep.sequenceNumberkyc.service.ts:1057updateKycStepAndLogkycStep.sequenceNumberupdateKycStep(2),updateBeneficialOwnerData,updateOperationActivityData,updatePaymentDatakyc.service.ts:1709,:1721,:1724,:1728reviewNationalityDatakycStep.sequenceNumberupdateNationalityStepFor the financial path:
inProgress()is the pattern the other result writes on a running step already use(
kyc.service.ts:350,:1140,:1162). It never putssequenceNumberinto the partial, so thevalue stays on the entity instead of being wiped. It forces no status transition here:
getPendingStepOrThrowthrows unless the stepisInProgress, i.e.status === IN_PROGRESSalready.Why not one change inside
update()Filtering the omitted arguments inside
update()would cover all four at once, but that method has11 production call sites, several of them in
mergeUserData. Two consequences, both unwanted:repairing the
statuswipe makes merged slave steps visible tocheckDfxApproval, which writes oldsteps to
OUTDATEDand sends akycStepRemindermail to the master — customer-visible, untested,unmeasured. And
pendingRecommendation.update(ReviewStatus.COMPLETED)(user-data.service.ts:1631)omits
sequenceNumberas well, so even the narrower variant would change in-memory behaviour on themerge path. Fixing the call sites keeps every one of those untouched:
kyc-step.entity.tsanduser-data.service.tsare not in this diff at all. #4540 (draft) holds the full analysis of thatpath, including the measurement (791 accounts with a completed video ident step, 27 without
identificationType = VideoId, 2 of them touched since 2024-11-11).The one remaining
update()without the argument ischeckDfxApproval(kyc.service.ts:438,expiredStep.update(OUTDATED, …)). It is deliberately left alone: notoStepBasefollows it, so noresponse is affected.
One difference not visible in the diff: the financial call previously also wrote
comment(viaaddComment(undefined), which returns the existing comment, or''when there is none).inProgress()does not touchcomment. Every consumer reads it through?.split(';')or atruthiness check, so
''andNULLare indistinguishable to all of them — no behaviour change, andit stops a pointless
NULL → ''write.Coverage
CONTRIBUTING.mdrequires every touched file at 100% on all four metrics and pinned in the same PR.src/subdomains/generic/kyc/services/kyc.service.tsis pinned inPINNED_LOGIC.Measured on this revision with the coverage-gate compile settings (
jest.coverage-gate.config.js,tsconfig.coverage.json) on the fivekyc.service*.spec.tsfiles:src/subdomains/generic/kyc/services/kyc.service.tsTo reach 100% branches, two unreachable or redundant fragments in
completeIdent/getIdentCheckErrorsare dropped:else if (nationality)/NATIONALITY_MISSINGfail after the outerifalready requirednationality— that fail branch could not run.Util.includesSameNameargument order. The helper is the word-set intersection, sothe two directions are the same.
Tests
Four original response tests in
kyc.service.spec.ts— one per call site — asserting on theresponse of the service method rather than on the entity, each pinning a concrete
sequenceNumber(7, 4, 5) instead oftoBeDefined(), so a wrong value fails too.Four additional spec files cover the rest of
kyc.service.tsso the file can be pinned.On this revision, under the coverage-gate compile:
kyc.service*.spec.tsfilesupdate(undefined, responses)Expected InProgress, Received undefined)linton the changed TypeScript filestype-checkformat:checkCI on head
73238d0a6(all required and reported checks): Build and checks, Test shards 1–3, Coverage, Coverage ratchet, Read-path projections, Full-stack E2E, review, CodeQL, Analyze — all SUCCESS. Shard totals: 177/176/175 suites passed; 3868/3924/3709 tests passed.Scope
Not in this PR: any change to
KycStep.update(), tomergeUserData, or to the video-ident branchthere — all three stay exactly as they are on develop, and neither file appears in the diff. No DTO
change, no
complete/missingFieldsextension (that was #4426, closed), no change toFinancialService.Not verified: no end-to-end run against the live endpoint — the tests drive the service with a
mocked repository. Full-stack E2E on this head is green, but that harness does not exercise
PUT /kyc/data/financial/:id. And the number that would carry the necessity is still missing: itis not established that any client currently fails because of the missing fields. What is
established is that the response contradicts the declared contract.
Final pass (73238d0):
Coherent: the four call-site writes restore the required response fields; the extra specs and
the
PINNED_LOGICentry exist only because CONTRIBUTING requires 100% on the whole touched file;the two production deletions drop an unreachable
NATIONALITY_MISSINGfail and a symmetricincludesSameNamecall so the 100% branch count is honest.Nothing extra:
KycStep.update(),mergeUserData, and the leftovercheckDfxApprovalupdate(OUTDATED)stay as they are on develop (#4540). The ratchet listed 97 other unpinnedcomplete files; they are not this PR's files and are not pinned here.
Sources closed: TaprootFreak CHANGES_REQUESTED on
c0b4b07b(100% four metrics, pin,per-file numbers) — done on this head. Mara APPROVED
c0b4b07b; this head needs a fresh review. #4893 test hygiene left as the stated non-blocking follow-up. The original “status doesnot change” report is still not claimed closed.