You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A completed RealUnit registration lifts the account to KYC level 20 (RealUnitService.ensureRegistrationKycLevel), and KycLevel.LEVEL_20 is defined in the enum as personal data. It does not close the PersonalData KYC step that level represents.
For an account that already carries an open PersonalData step from an earlier, abandoned KYC attempt, that step stays IN_PROGRESS permanently:
RealUnitService.completeRegistration writes personal data through UserDataService.updatePersonalData, which does not touch the step. Only KycService.updatePersonalData completes it, and the RealUnit client never calls that endpoint.
The auto-complete in KycService.createStep — if (user.isDataComplete && !preventDirectEvaluation) — cannot fire, because the presence of a prior step row sets preventDirectEvaluation.
KycInfoMapper.toDto then hands that stale step back as currentStep.
The RealUnit client cannot render PersonalData (_mapStepName in kyc_cubit.dart covers only a subset of step names and falls through to null), so it shows a fatal "Error while loading" screen. The user is permanently blocked from onboarding with no path forward in the app.
Every request in the flow returns 200/201 and nothing is logged above LOG level, so this failure is invisible in monitoring — it only surfaces through user reports.
Fix, not a behaviour change
The API already asserts the personal data is satisfied by granting level 20; the open step contradicts an assertion the API itself made. Completing it grants no new level and changes no verification requirement — Ident and everything above it stay exactly as they are.
Scope
API
1. Close a satisfied PersonalData step on registration. In completeRegistration, complete a pending step when isDataComplete, mirroring what KycService.updatePersonalData already does. Must run in both the hasExistingData and !hasExistingData branches — the former currently skips personal-data handling entirely. Must not resurrect FAILED steps: preventDirectEvaluation exists so a retry does not paper over a prior rejection, so scope this to IN_PROGRESS only (getPendingStepWith). completeRegistration has already run isPersonalDataMatching and thrown a 400 on mismatch before this point, so the data is verified equal to what was signed.
No migration required: registration is a mandatory gate, so affected accounts repair themselves on the path they have to take anyway.
The inconsistency is real: KycInfoMapper.toDto scopes kycSteps[].isRequired and processStatus by contextRequiredSteps, but selects currentStep before requiredStepNames exists and never consults it. Two reasons not to fix it:
The obvious fix is harmful.contextRequiredSteps(RealunitBuy) is narrower than requiredKycSteps, and several globally-required steps that requiredKycSteps orders beforeIdent sit outside it — the organization/sole-proprietorship steps (LegalEntity, OwnerDirectory, SignatoryPower, Authority, OperationalActivity, BeneficialOwner, SoleProprietorshipConfirmation) and RecallAgreement for personal accounts with recallAgreementAccepted === false. Those steps gate the in-context ones: updateProgress runs only when !user.hasStepsInProgress, and that getter counts any open step. Scoping the fallback therefore hides the step that is blocking progress, answering InProgress with no step and no session URL — permanently, since the open step blocks getNext.
The safe version is unobservable. Narrowing only once the context's own steps are settled avoids the above, but currentStep is read only under processStatus: InProgress — exactly the case that correction stops narrowing. Completed and PendingReview are routed without it, and kycSteps[].isCurrent is parsed by the client and never read.
Revisit only if a client starts consuming currentStep or isCurrent outside the InProgress path.
Client — RealUnitCH/app
3. Send the KYC context on the onboarding entry point. It currently navigates to /kyc with no extra, so kycContext is null and nothing is scoped. Does not depend on 2 and is worth doing on its own: with no context, processStatus is computed over all of requiredKycSteps including FinancialData and DfxApproval, which RealunitBuy deliberately excludes — so a user who has finished Ident is still reported InProgress for steps that do not gate buying. One line.
4. Add the step pages still missing inside the RealunitBuy context:Recommendation and ResidencePermit. There is no generic webview escape hatch — KycStep.sessionInfo returns UrlType.API for these (an endpoint expecting a typed PUT, not a renderable page), so they need native forms.
5. Stop treating an unmapped step as fatal. Render a handoff the user can act on and report the occurrence, so the next gap in the mapping is visible rather than silent.
Suggested order
1 → 3 + 5 → 4. Item 2 is dropped (see above).
Item 1 alone removes the dominant cause, changes no client contract, and is safe to ship on its own. Items 3 and 5 are small and share the same two client files; item 4 adds screens and golden tests, so it belongs in its own PR.
Problem
A completed RealUnit registration lifts the account to KYC level 20 (
RealUnitService.ensureRegistrationKycLevel), andKycLevel.LEVEL_20is defined in the enum as personal data. It does not close thePersonalDataKYC step that level represents.For an account that already carries an open
PersonalDatastep from an earlier, abandoned KYC attempt, that step staysIN_PROGRESSpermanently:RealUnitService.completeRegistrationwrites personal data throughUserDataService.updatePersonalData, which does not touch the step. OnlyKycService.updatePersonalDatacompletes it, and the RealUnit client never calls that endpoint.KycService.createStep—if (user.isDataComplete && !preventDirectEvaluation)— cannot fire, because the presence of a prior step row setspreventDirectEvaluation.KycInfoMapper.toDtothen hands that stale step back ascurrentStep.The RealUnit client cannot render
PersonalData(_mapStepNameinkyc_cubit.dartcovers only a subset of step names and falls through tonull), so it shows a fatal "Error while loading" screen. The user is permanently blocked from onboarding with no path forward in the app.Every request in the flow returns 200/201 and nothing is logged above
LOGlevel, so this failure is invisible in monitoring — it only surfaces through user reports.Fix, not a behaviour change
The API already asserts the personal data is satisfied by granting level 20; the open step contradicts an assertion the API itself made. Completing it grants no new level and changes no verification requirement — Ident and everything above it stay exactly as they are.
Scope
API
1. Close a satisfied
PersonalDatastep on registration. IncompleteRegistration, complete a pending step whenisDataComplete, mirroring whatKycService.updatePersonalDataalready does. Must run in both thehasExistingDataand!hasExistingDatabranches — the former currently skips personal-data handling entirely. Must not resurrectFAILEDsteps:preventDirectEvaluationexists so a retry does not paper over a prior rejection, so scope this toIN_PROGRESSonly (getPendingStepWith).completeRegistrationhas already runisPersonalDataMatchingand thrown a 400 on mismatch before this point, so the data is verified equal to what was signed.2. Scope the— investigated and deliberately dropped (fix(kyc): scope the currentStep fallback to the KYC context #4591, closed unmerged). Do not re-attempt without reading this.currentStepfallback to the KYC context.The inconsistency is real:
KycInfoMapper.toDtoscopeskycSteps[].isRequiredandprocessStatusbycontextRequiredSteps, but selectscurrentStepbeforerequiredStepNamesexists and never consults it. Two reasons not to fix it:The obvious fix is harmful.
contextRequiredSteps(RealunitBuy)is narrower thanrequiredKycSteps, and several globally-required steps thatrequiredKycStepsorders beforeIdentsit outside it — the organization/sole-proprietorship steps (LegalEntity,OwnerDirectory,SignatoryPower,Authority,OperationalActivity,BeneficialOwner,SoleProprietorshipConfirmation) andRecallAgreementfor personal accounts withrecallAgreementAccepted === false. Those steps gate the in-context ones:updateProgressruns only when!user.hasStepsInProgress, and that getter counts any open step. Scoping the fallback therefore hides the step that is blocking progress, answeringInProgresswith no step and no session URL — permanently, since the open step blocksgetNext.The safe version is unobservable. Narrowing only once the context's own steps are settled avoids the above, but
currentStepis read only underprocessStatus: InProgress— exactly the case that correction stops narrowing.CompletedandPendingRevieware routed without it, andkycSteps[].isCurrentis parsed by the client and never read.Revisit only if a client starts consuming
currentSteporisCurrentoutside theInProgresspath.Client — RealUnitCH/app
/kycwith noextra, sokycContextis null and nothing is scoped. Does not depend on 2 and is worth doing on its own: with no context,processStatusis computed over all ofrequiredKycStepsincludingFinancialDataandDfxApproval, whichRealunitBuydeliberately excludes — so a user who has finishedIdentis still reportedInProgressfor steps that do not gate buying. One line.RealunitBuycontext:RecommendationandResidencePermit. There is no generic webview escape hatch —KycStep.sessionInforeturnsUrlType.APIfor these (an endpoint expecting a typedPUT, not a renderable page), so they need native forms.Suggested order
1 → 3 + 5 → 4. Item 2 is dropped (see above).
Item 1 alone removes the dominant cause, changes no client contract, and is safe to ship on its own. Items 3 and 5 are small and share the same two client files; item 4 adds screens and golden tests, so it belongs in its own PR.