Skip to content

fix(kyc): scope the currentStep fallback to the KYC context - #4591

Closed
Danswar wants to merge 1 commit into
developfrom
fix/kyc-scope-current-step-to-context
Closed

fix(kyc): scope the currentStep fallback to the KYC context#4591
Danswar wants to merge 1 commit into
developfrom
fix/kyc-scope-current-step-to-context

Conversation

@Danswar

@Danswar Danswar commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Closes item 2 of #4556. Independent of #4559 — no file overlap, no code dependency in either direction.

Problem

KycInfoMapper.toDto produces three context-sensitive things, and two of the three already honour the scope:

field scoped by context?
kycSteps[].isRequired yes — requiredStepNames.has(s.name)
processStatus yes — filters requiredSteps before computing the verdict
currentStep no — selected before requiredStepNames even exists, and never consults it

So a step irrelevant to the flow — an open AddressChange, PhoneChange or PaymentAgreement — can hijack currentStep while processStatus reports the context as satisfied. The caller is then sent to something its flow never asked for.

Change

The fallback obeys the same scope the verdict already does. This is a consistency fix rather than a new policy: the semantics were decided when contextRequiredSteps was introduced; currentStep was simply left out.

Deliberate constraints

  • Scoped by the context set, not by requiredStepNames. AddressChange, PhoneChange, NameChange, PaymentAgreement and AdditionalDocuments are not members of requiredKycSteps at all, so filtering by it would drop them from currentStep for every caller and break the change flows. There is a regression test for exactly this.
  • Opt-in by construction. context is an optional, @IsEnum-validated query param, and only RealunitBuy narrows anything (RealunitSell / RealunitTransfer return undefined). A caller that passes no context gets a byte-identical response, so no other client is affected regardless of who they are.
  • Only the fallback is scoped. tryContinue passes currentStep as undefined, so the fallback is what answers PUT /v2/kyc; the one caller that does pass an explicit step (getOrCreateStep) never passes a context, so it is untouched.

Verification

  • 5 new tests, each mutation-checked: removing the scoping, leaving the FAILED fallback context-blind, and scoping by requiredStepNames instead of the context set all turn the suite red.
  • The two picking tests use OwnerDirectory (out of context) against ResidencePermit (in context) because the step order is the enum order — a later-sorting out-of-context step would have passed either way and proved nothing.
  • Full kyc subdomain green (113 passed), tsc --noEmit clean, eslint and prettier clean.

Note for the client side

With the context satisfied, processStatus is already Completed, so the RealUnit client renders its completed screen and never reads currentStep. The undefined case is only reachable when processStatus is InProgress, which by definition means an in-context step is actionable. Client-side handling of unmappable steps is tracked separately in RealUnitCH/app#613.

processStatus already reports only over the context's steps, but the
currentStep fallback was selected before requiredStepNames even existed and
never consulted it. A step irrelevant to the flow - an open AddressChange,
PhoneChange or PaymentAgreement - could therefore hijack currentStep and
send the caller off to something its flow never asked for, while
processStatus reported the context as satisfied.

Scoped by the CONTEXT set, deliberately not by requiredStepNames: those
change steps are not members of requiredKycSteps at all, so filtering by it
would drop them from currentStep for every caller and break the change
flows. With no context nothing is narrowed, so callers that pass none get a
byte-identical response.

Only the fallback is scoped. tryContinue passes currentStep undefined, so
the fallback is what answers PUT /v2/kyc; the one caller that does pass an
explicit step never passes a context, so it is untouched.
@Danswar

Danswar commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Closing without merging: the change is harmful as written, and correcting it leaves nothing observable.

The regression

contextRequiredSteps(RealunitBuy) is narrower than requiredKycSteps. Several globally-required steps that requiredKycSteps orders before Ident are outside the context set — LegalEntity, SoleProprietorshipConfirmation, OwnerDirectory, SignatoryPower, Authority, OperationalActivity, BeneficialOwner for organization and sole-proprietorship accounts, and RecallAgreement for any personal account with recallAgreementAccepted === false.

Those steps gate the in-context ones: updateProgress is entered only when !user.hasStepsInProgress, and that getter counts any open step regardless of name. So while one of them is open, getNext cannot create the in-context step.

Scoping the fallback hides exactly the step that is blocking progress:

scenario, context=RealunitBuy before with this change
ORG account, LegalEntity in progress, Ident not started InProgress / currentStep=LegalEntity InProgress / currentStep=undefined
Personal account, RecallAgreement in progress, Ident not started InProgress / currentStep=RecallAgreement InProgress / currentStep=undefined

The caller is told it has actionable work and given no step and no session URL, and it cannot self-heal — the open step blocks getNext indefinitely. That also contradicts the documented contract on KycLevelDto.processStatus ("InProgress ⇒ at least one required step is actionable by the user").

Why correcting it is not worth merging

The fix is to narrow only once the context's own steps are settled. That works, but it makes the change unobservable:

  • currentStep is read by the client only when processStatus is InProgress — precisely the case the correction stops narrowing.
  • processStatus: Completed and PendingReview are routed without consulting currentStep.
  • The other affected field, kycSteps[].isCurrent, is parsed by the client and never read.

The underlying inconsistency is real — isRequired and processStatus honour the context while currentStep does not — but it has no reachable consequence today, and the naive correction of it does. Recorded on #4556 so it is not re-derived.

The hijack this PR set out to prevent (an open AddressChange / PhoneChange / PaymentAgreement surfacing as currentStep) only occurs once the context is satisfied, which is exactly when no client reads the field.

@Danswar Danswar closed this Aug 2, 2026
@Danswar
Danswar deleted the fix/kyc-scope-current-step-to-context branch August 2, 2026 02:11
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