Skip to content

fix(kyc): report submission completeness for personal and financial data - #4426

Closed
joshuakrueger-dfx wants to merge 3 commits into
developfrom
fix/kyc-submit-completeness-feedback
Closed

fix(kyc): report submission completeness for personal and financial data#4426
joshuakrueger-dfx wants to merge 3 commits into
developfrom
fix/kyc-submit-completeness-feedback

Conversation

@joshuakrueger-dfx

@joshuakrueger-dfx joshuakrueger-dfx commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Why

A tester reported from the sandbox: "after submitting the financial information the status does not change at all".

Root cause: updateFinancialData always stores the submitted answers (draft behaviour, intended), but only advances the step when FinancialService.isComplete is satisfied. On an incomplete submission the client receives HTTP 200 with an unchanged step and no indication that anything is missing — it looks like a successful no-op.

The client cannot determine completeness on its own either: isComplete evaluates conditional questions via q.conditions, and while getQuestions did return conditions in the JSON, the field was never declared in the API contract, so generated clients do not know about it.

updatePersonalData had the same class of defect with user.isDataComplete.

Solution

Both submit endpoints now return KycStepSubmitDto (additive, extends KycStepBase):

  • complete: boolean — whether the submission fulfilled the required fields and advanced the step
  • missingFields: string[] — which required entries are still missing

Completeness has a single source of truth: FinancialService.getMissingFields is authoritative and isComplete delegates to it; likewise UserData.missingKycFields with isDataComplete. conditions is now declared in the Swagger contract.

For personal data the missing entries are mapped to the request field paths (firstnamefirstName, locationaddress.city, …), because the entity names do not exist in KycPersonalData. mail is deliberately excluded — it cannot be set through this endpoint (that is the ContactData step), so reporting it would be unactionable for the client.

Scope / non-goals

  • No status code change: incomplete submissions stay HTTP 200 and keep storing the draft.
  • computeProcessStatus is untouched. That an unsubmitted required step keeps the overall status at "in progress" is verified, intended behaviour.
  • updatePaymentData untouched.
  • No migration, no entity columns, no changes to existing KycStepBase fields.

Behaviour preserved deliberately

The pre-existing every() short-circuit is retained: once a required answer is missing, option values of later questions are no longer validated. Same input, same throw behaviour as before — otherwise a draft submission containing a stale option value would have started returning 400 instead of 200. Covered in both directions by tests.

Tests

105 tests green (from 92). Every new test was verified to fail without its corresponding change, each fix removed individually:

  • financial completeness / conditional questions: 3 tests fail when the condition guard is removed
  • short-circuit preservation: 1 test fails when the guard is removed
  • step state in the draft response: 4 tests fail when the status pass-through is reverted
  • personal data field mapping: 2 tests fail when a single mapping entry is corrupted

Gates: npm run lint, npm run format:check, npx tsc --noEmit, npx jest src/subdomains/generic/kyc — all green.

Known gaps

  • No end-to-end run against a live API or the sandbox environment.
  • Consumer synchronisation is a follow-up, tracked separately: the response type in @dfx.swiss/react needs the two new fields, and DFXswiss/services needs to surface them in the form. The change here is purely additive and breaks no existing client, but the reported sandbox symptom only disappears once a consumer reads them.

mail is filtered out of the personal data response as a defensive measure only. It cannot occur in the regular flow: requiredKycSteps orders ContactData before PersonalData and getNext always returns the first unfinished step, so the personal data step is unreachable while the mail address is unset.

The financial and personal data submit endpoints stored incomplete drafts and
returned HTTP 200 without advancing the step, leaving clients unable to tell
that anything was missing. Both endpoints now return the completeness state
and the missing field keys.

Also declares the previously undocumented question conditions in the API
contract, so clients can determine which questions apply.
…esponse

The financial draft path passed an undefined status into KycStep.update, which
cleared status and sequenceNumber on the returned entity, so the response
omitted two required fields. The personal data endpoint reported entity field
names that do not exist in the request contract; they are now mapped to the
request field paths, and the mail field is omitted because it cannot be set
through this endpoint.
Object.assign copied undefined values, so calling update without a status or
sequence number cleared both on the in-memory entity. Callers that only wanted
to store a result therefore returned responses without two required fields, and
the previous call-site workaround wrote a possibly stale status back to the
database. Dropping undefined values at the source keeps the write behaviour
unchanged and fixes the file upload path as well.
@joshuakrueger-dfx

Copy link
Copy Markdown
Collaborator Author

Closing in favour of two narrower changes. Reviewing this branch against develop turned up three things that make the split the better shape.

1. The reported symptom is fixed by six lines, not by the DTO extension.
updateFinancialData calls kycStep.update(undefined, data.responses), and Object.assign copied that undefined onto the entity. KycStepMapper.toStepBase then mapped StepMap[undefined] to undefined, so an incomplete draft submit returned HTTP 200 without status and without sequenceNumber — both declared required on KycStepBase. A client keying off status sees nothing change, which is the sandbox report.

2. That same fix reactivates a branch in mergeUserData that has been dead since 2024-11-11.
The cancel loop passes status: undefined for every slave step not in the cancel list, COMPLETED included. With the status wiped, slave.kycSteps.some(k => (VIDEO || SUMSUB_VIDEO) && k.isCompleted) — which sets identificationType = VIDEO_ID and bankTransactionVerification = UNNECESSARY — could never be true. That rule was added deliberately in #1424 (2024-07-09) and silently disabled by 6fb9d18536 ([NOTASK] fix userData merge). Restoring it changes an AML-relevant outcome on account merges, so it belongs in a change whose title says so and which carries its own test.

3. missingFields on PUT /kyc/data/personal/:id is always empty in production.
The global ValidationPipe rejects any incomplete body with 400 (KycPersonalData and KycAddress are @IsNotEmpty throughout, organisation fields via @ValidateIf), and UserDataService.updatePersonalData writes every one of them to the entity. The only required field that can still be unset afterwards is mail, which the mapping deliberately drops — so the mapping table, the type guard and their spec compute a constant [].

The description also claimed the personal-data step is unreachable while mail is unset. It is not: GET /v2/kyc/:step creates any step by name with no ordering gate, and mail is null by default for a fresh wallet user. That makes complete: false, missingFields: [] the only reachable non-trivial case on that endpoint.

Replaced by:

complete / missingFields is deliberately not carried over: it is only worth its surface once the reported response body shows that the two changes above do not already cover the case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant