Problem
After the user confirms a merge, the backend continues processing (step re-parenting, KYC follow-up, level transition, mail). The polling status endpoint that the client uses while the merge processes — KycController.getKycInfo (src/subdomains/generic/kyc/controllers/kyc.controller.ts:119–124 → KycService.getInfo → KycInfoMapper.toDto) — has no way to communicate "your merge is still being processed". The response is computed synchronously from userData + kycSteps and never references the AccountMerge lifecycle.
The AccountMerge entity (src/subdomains/generic/user/models/account-merge/account-merge.entity.ts) tracks only:
isCompleted: boolean (default false)
expiration: Date
- derived
isExpired = expiration < now()
…with no intermediate "user-confirmed, backend processing" state. Once executeMerge() runs, isCompleted flips to true (account-merge.entity.ts:48–58). Between the user clicking "confirm" and the merge committing, the entity row says isCompleted = false, !isExpired — indistinguishable from "merge requested, not yet confirmed".
User impact
The realunit-app polls the status endpoint with a 30-second client-side timeout. When the merge is still processing (e.g. checkDfxApproval follow-up running, see #3801), the timeout fires and the user sees "Fehler beim Laden" even though nothing actually failed. The accompanying app#611 already plans to drop the local timeout interpretation, but the app cannot render a meaningful waiting state until the API exposes one.
Suggested fix shape
Two parts, both additive on the API side:
-
Add a processing marker to AccountMerge. Smallest change: one new nullable timestamp column (e.g. processingStartedAt: Date | null). Set when executeMerge() begins; clear when it finishes (or when isCompleted = true). The "merge in progress" predicate is then isCompleted = false && processingStartedAt != null && !isExpired.
-
Surface the predicate in the status response. Either:
- Extend
KycLevelDto with a new optional field (e.g. accountMergeState?: 'requested' | 'processing'), populated by KycInfoMapper.toDto after a single query against accountMergeRepo.
- Or extend the existing
KycProcessStatus enum (kyc-info.dto.ts) with a new value like MergeProcessing, returned by computeProcessStatus when an in-flight merge exists for the userData. This is the form aligned with how the app already routes on processStatus (Wave 2 / app#494).
Either form is additive; old clients ignore the new field / treat the new enum value as a no-op.
Pair-PR
Companion app PR required: realunit-app renders the new state as a waiting screen (with appropriate copy) instead of interpreting the polling timeout as failure. The pair-PR contract is documented in realunit-app/CONTRIBUTING.md:23–67 ("API as Decision Authority") and the wave plan in realunit-app/docs/api-authority-plan.md.
Related
#3801 (KYC level-50 completion mail delayed ~20 min) overlaps in trigger and benefits from the same event-driven post-merge dispatch.
#3800 (slave KycStep FK not reassigned) is independent but also under "post-merge correctness".
Source
Surfaced in DFXswiss/realunit-app#611 (item 5).
Problem
After the user confirms a merge, the backend continues processing (step re-parenting, KYC follow-up, level transition, mail). The polling status endpoint that the client uses while the merge processes —
KycController.getKycInfo(src/subdomains/generic/kyc/controllers/kyc.controller.ts:119–124→KycService.getInfo→KycInfoMapper.toDto) — has no way to communicate "your merge is still being processed". The response is computed synchronously fromuserData+kycStepsand never references theAccountMergelifecycle.The
AccountMergeentity (src/subdomains/generic/user/models/account-merge/account-merge.entity.ts) tracks only:isCompleted: boolean(defaultfalse)expiration: DateisExpired = expiration < now()…with no intermediate "user-confirmed, backend processing" state. Once
executeMerge()runs,isCompletedflips totrue(account-merge.entity.ts:48–58). Between the user clicking "confirm" and the merge committing, the entity row saysisCompleted = false, !isExpired— indistinguishable from "merge requested, not yet confirmed".User impact
The realunit-app polls the status endpoint with a 30-second client-side timeout. When the merge is still processing (e.g.
checkDfxApprovalfollow-up running, see#3801), the timeout fires and the user sees "Fehler beim Laden" even though nothing actually failed. The accompanyingapp#611already plans to drop the local timeout interpretation, but the app cannot render a meaningful waiting state until the API exposes one.Suggested fix shape
Two parts, both additive on the API side:
Add a processing marker to
AccountMerge. Smallest change: one new nullable timestamp column (e.g.processingStartedAt: Date | null). Set whenexecuteMerge()begins; clear when it finishes (or whenisCompleted = true). The "merge in progress" predicate is thenisCompleted = false && processingStartedAt != null && !isExpired.Surface the predicate in the status response. Either:
KycLevelDtowith a new optional field (e.g.accountMergeState?: 'requested' | 'processing'), populated byKycInfoMapper.toDtoafter a single query againstaccountMergeRepo.KycProcessStatusenum (kyc-info.dto.ts) with a new value likeMergeProcessing, returned bycomputeProcessStatuswhen an in-flight merge exists for the userData. This is the form aligned with how the app already routes onprocessStatus(Wave 2 /app#494).Either form is additive; old clients ignore the new field / treat the new enum value as a no-op.
Pair-PR
Companion app PR required: realunit-app renders the new state as a waiting screen (with appropriate copy) instead of interpreting the polling timeout as failure. The pair-PR contract is documented in
realunit-app/CONTRIBUTING.md:23–67("API as Decision Authority") and the wave plan inrealunit-app/docs/api-authority-plan.md.Related
#3801(KYC level-50 completion mail delayed ~20 min) overlaps in trigger and benefits from the same event-driven post-merge dispatch.#3800(slave KycStep FK not reassigned) is independent but also under "post-merge correctness".Source
Surfaced in
DFXswiss/realunit-app#611(item 5).