fix(account-merge): stop stale-collection save from orphaning re-parented KYC steps#3805
Conversation
|
ReviewVerdict: Solves #3800 for future merges ( Blocking1. Backfill for existing data is missing. The fix is forward-only. Already-merged UserDatas in PRD have orphan UPDATE kyc_step
SET userDataId = ud.masterId
FROM user_data ud
WHERE ud.id = kyc_step.userDataId
AND ud.status = 'Merged'
AND ud.masterId IS NOT NULL;Without this, #3800 remains unresolved for historical cases. Non-blocking2. Sequence-collision risk in 3. Missing logger call. 4. Test coverage gaps. Spec only verifies the mock echo (
IdempotencySafe on retry — CI: 7/7 green. |
|
Thanks @TaprootFreak — addressed in 0cb523f: Blocking #1 — backfill: added migration UPDATE "kyc_step" SET "userDataId" = am."masterId"
FROM "account_merge" am
WHERE am."slaveId" = "kyc_step"."userDataId" AND am."isCompleted" = truelooped until no orphans remain (resolves chained merges A→B→C) and idempotent. Worth a dry-run on a PRD snapshot before apply. Non-blocking #2 — seq collision: can't actually occur on re-parent: the original Non-blocking #3 — logger: Suite green (72 suites). |
ReviewProblem statement is incorrectThe PR says The explicit
|
|
@davidleomay good catch — you're right, and I'll correct the PR description.
Reframing the PR: the explicit On the backfill migration: agreed it assumes the slave offset was persisted (post-2024-11-11 via |
mergeUserData only concatenated the slave's kycSteps onto the master in-memory. The slave rows kept userData_id = slave.id, so on the next reload of the master they vanished and already-completed steps (e.g. FINANCIAL_DATA) were re-flagged as missing. Add KycAdminService.reassignKycSteps and call it after the slave-step update loop so the FK is persisted. Closes #3800
…gnment Address review on #3800: - Add a data migration that re-parents KYC steps still pointing at merged-away (slave) accounts onto their surviving master, using the completed account_merge rows as the slave->master mapping (user_data has no master FK). Looped to resolve chained merges; idempotent. - Log the affected step count in reassignKycSteps for audit trails.
0cb523f to
610abd4
Compare
|
@davidleomay I dug into the merge flow with your hypothesis in mind — you're right on both counts. Walking the code: 1.
|
|
The redundant There are 2083 orphaned kyc_step rows on merged userDatas in prod, confirming the stale-save race is real. A backfill migration has value, but should be paired with the Please close this and open a dedicated PR that fixes the |
…d KYC steps Address David May's review: drop the redundant reassignKycSteps and fix the real root cause. updateUserData loads kycSteps+users and `save(userData)` makes TypeORM's OneToManySubjectBuilder reconcile those collections — so a master loaded with a pre-merge snapshot nulls out the slave steps a merge just re-parented (2083 orphaned kyc_step rows in prod). Persist scalars/FKs only by excluding the loaded OneToMany collections from the saved object; the returned entity keeps them for the HTTP response. - remove KycAdminService.reassignKycSteps (the concat + save(master) in mergeUserData already persists the re-parent) + its call + its spec - keep the backfill migration (pairs with the fix so no new orphans appear) - add regression test: updateUserData's save() excludes kycSteps/users
|
@davidleomay done — repurposed this PR to your guidance (commit 9d818c2):
Full suite green (twice). PR title/description updated to the corrected root-cause framing. |
Backfill of 1510 orphaned kyc_step rows was run manually against prod. 573 remain from pre-account_merge era (Nov 2023) with no resolvable master — harmless on dead Merged accounts.
davidleomay
left a comment
There was a problem hiding this comment.
Backfill of 1510 orphaned kyc_step rows was executed manually. 573 remain from pre-account_merge era (Nov 2023, no resolvable master, harmless). Migration removed from PR. Code fix looks good.
Problem (corrected per @davidleomay review)
The original framing was wrong: the
concat+save(master)inmergeUserDataalready persists the slave→master FK re-parent (TypeORMOneToManySubjectBuilder). The real root cause of #3800 is a stale-collection save:updateUserDataloadsrelations: { users, kycSteps, wallet }and thensave(userData).save()reconciles loaded@OneToManycollections, so when an admin edits a master whosekycSteps/userssnapshot predates a merge, the builder sets the just-re-parented slave rows back touserDataId = NULL. Confirmed by 2083 orphanedkyc_steprows on merged userDatas in prod.Fix
updateUserData, persist scalars/FKs only by excluding the loaded@OneToManycollections (kycSteps,users) from the saved object —save()can no longer orphan rows a concurrent merge re-parented. The returned entity keeps the collections for the HTTP response; user changes are persisted separately viauserRepo.setUserRef.KycAdminService.reassignKycSteps(and its call + spec) —save(master)already persists the re-parent.BackfillMergedKycStepUserData(pairs with the fix so no new orphans appear after each merge).updateUserData'ssave()payload excludeskycSteps/users.Verification
type-check,lint,format:check,build, and the full DB-free unit suite (twice, deterministic) all green; backfill is Postgres-valid (migration-psql-check).