Context
PR #3794 fixed the user-visible symptom of a KycStepName.DFX_APPROVAL step reaching the client as a user-actionable currentStep with processStatus = InProgress, which has no UI on the client side (blank screen on RealUnit app, see realunit-app#618).
That fix is at the DTO boundary in KycInfoMapper. Stacked PR #3811 hardens the test coverage of the same boundary and adds a logger.warn so we get visibility when the broken state surfaces.
Neither PR touches the upstream cause: why does a DFX_APPROVAL step ever persist in ReviewStatus.IN_PROGRESS long enough for the mapper to see it?
Expected behaviour
By design, every initiateStep path for DFX_APPROVAL transitions the step away from IN_PROGRESS before save:
src/subdomains/generic/kyc/services/kyc.service.ts:1337-1348 — kycLevel >= LEVEL_50 → complete(); no missing steps → manualReview(); otherwise → onHold().
kycStep.complete() / manualReview() / onHold() are synchronous mutations on the entity (src/subdomains/generic/kyc/entities/kyc-step.entity.ts:234,331,339).
The default IN_PROGRESS from KycStep.create() (src/subdomains/generic/kyc/entities/kyc-step.entity.ts:157) should be a transient in-memory state, never persisted for DFX_APPROVAL.
Actual behaviour
Empirically the mapper has seen DFX_APPROVAL steps in actionable status (covered by the new tests in #3794 / #3811). Possible vectors to investigate:
- A code path that constructs a
DFX_APPROVAL step without going through initiateStep (direct KycStep.create + save).
- An error path in
initiateStep that saves the step before the switch transitions it.
- A retry / restart / repair path that re-emits the step in
IN_PROGRESS.
- Historical data: legacy rows from before the transition logic was added.
Proposal
- Audit every write site for
DFX_APPROVAL (grep KycStep.create, kycStepRepo.save, manual entity construction) and confirm no path persists IN_PROGRESS.
- Optionally tighten
KycStep.create() so a DFX_APPROVAL (and any future name in KycStepNonUserActionable) cannot be constructed without an explicit non-actionable status — moves the invariant into the factory.
- Or, narrower: assert in
kycStepRepo.save (or via a pre-save hook) that no KycStepNonUserActionable row leaves the service in IN_PROGRESS.
- Backfill: a one-off migration scanning for existing rows in this state and routing them to
manualReview / onHold based on the user's kycLevel and missing-steps set.
Once the root cause is fixed, the mapper guard from #3794 and the logger.warn from #3811 become belt-and-braces — no behaviour change on the client, but the warn line stops firing in production logs, which is the canary.
References
Context
PR #3794 fixed the user-visible symptom of a
KycStepName.DFX_APPROVALstep reaching the client as a user-actionablecurrentStepwithprocessStatus = InProgress, which has no UI on the client side (blank screen on RealUnit app, see realunit-app#618).That fix is at the DTO boundary in
KycInfoMapper. Stacked PR #3811 hardens the test coverage of the same boundary and adds alogger.warnso we get visibility when the broken state surfaces.Neither PR touches the upstream cause: why does a
DFX_APPROVALstep ever persist inReviewStatus.IN_PROGRESSlong enough for the mapper to see it?Expected behaviour
By design, every
initiateSteppath forDFX_APPROVALtransitions the step away fromIN_PROGRESSbefore save:src/subdomains/generic/kyc/services/kyc.service.ts:1337-1348—kycLevel >= LEVEL_50→complete(); no missing steps →manualReview(); otherwise →onHold().kycStep.complete()/manualReview()/onHold()are synchronous mutations on the entity (src/subdomains/generic/kyc/entities/kyc-step.entity.ts:234,331,339).The default
IN_PROGRESSfromKycStep.create()(src/subdomains/generic/kyc/entities/kyc-step.entity.ts:157) should be a transient in-memory state, never persisted forDFX_APPROVAL.Actual behaviour
Empirically the mapper has seen
DFX_APPROVALsteps in actionable status (covered by the new tests in #3794 / #3811). Possible vectors to investigate:DFX_APPROVALstep without going throughinitiateStep(directKycStep.create+ save).initiateStepthat saves the step before the switch transitions it.IN_PROGRESS.Proposal
DFX_APPROVAL(grepKycStep.create,kycStepRepo.save, manual entity construction) and confirm no path persistsIN_PROGRESS.KycStep.create()so aDFX_APPROVAL(and any future name inKycStepNonUserActionable) cannot be constructed without an explicit non-actionable status — moves the invariant into the factory.kycStepRepo.save(or via a pre-save hook) that noKycStepNonUserActionablerow leaves the service inIN_PROGRESS.manualReview/onHoldbased on the user'skycLeveland missing-steps set.Once the root cause is fixed, the mapper guard from #3794 and the
logger.warnfrom #3811 become belt-and-braces — no behaviour change on the client, but the warn line stops firing in production logs, which is the canary.References
KycStepNonUserActionable).FAILED/MANUAL_REVIEWDfxApproval andlogger.warnon dropped explicitcurrentStep.