From dc43679b6f98157b462bcf26f721aee86788e653 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:09:48 +0200 Subject: [PATCH] Expose MergeProcessing state on RealUnit registration during account merge --- .../__tests__/realunit.service.spec.ts | 42 +++++++++++++------ .../realunit/dto/realunit-registration.dto.ts | 3 +- .../supporting/realunit/realunit.service.ts | 9 +++- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/subdomains/supporting/realunit/__tests__/realunit.service.spec.ts b/src/subdomains/supporting/realunit/__tests__/realunit.service.spec.ts index ac87826230..22e55baf46 100644 --- a/src/subdomains/supporting/realunit/__tests__/realunit.service.spec.ts +++ b/src/subdomains/supporting/realunit/__tests__/realunit.service.spec.ts @@ -113,6 +113,7 @@ describe('RealUnitService', () => { let sellService: jest.Mocked; let userService: jest.Mocked; let kycService: jest.Mocked; + let accountMergeService: jest.Mocked; const realuAsset = createCustomAsset({ id: 1, @@ -190,7 +191,7 @@ describe('RealUnitService', () => { }, }, { provide: TransactionService, useValue: {} }, - { provide: AccountMergeService, useValue: {} }, + { provide: AccountMergeService, useValue: { hasProcessingMerge: jest.fn().mockResolvedValue(false) } }, { provide: RealUnitDevService, useValue: {} }, { provide: SwissQRService, useValue: {} }, { provide: FeeService, useValue: {} }, @@ -208,6 +209,7 @@ describe('RealUnitService', () => { sellService = module.get(SellService); userService = module.get(UserService); kycService = module.get(KycService); + accountMergeService = module.get(AccountMergeService); }); afterEach(() => { @@ -537,11 +539,25 @@ describe('RealUnitService', () => { }; } - it('returns state=ALREADY_REGISTERED when a non-failed step for the current wallet exists', () => { + it('returns state=MERGE_PROCESSING with no userData while an account merge for the user is still propagating', async () => { + accountMergeService.hasProcessingMerge.mockResolvedValue(true); const userData = buildVerifiedUserData(); userData.getStepsWith.mockReturnValue([buildStepForWallet(walletAddress)]); - const status = service.getRegistrationInfo(userData, walletAddress); + const status = await service.getRegistrationInfo(userData, walletAddress); + + expect(status.state).toBe(RealUnitRegistrationState.MERGE_PROCESSING); + expect(status.isRegistered).toBe(false); + expect(status.userData).toBeUndefined(); + // the merge guard short-circuits before any registration-step inspection + expect(userData.getStepsWith).not.toHaveBeenCalled(); + }); + + it('returns state=ALREADY_REGISTERED when a non-failed step for the current wallet exists', async () => { + const userData = buildVerifiedUserData(); + userData.getStepsWith.mockReturnValue([buildStepForWallet(walletAddress)]); + + const status = await service.getRegistrationInfo(userData, walletAddress); expect(status.state).toBe(RealUnitRegistrationState.ALREADY_REGISTERED); expect(status.isRegistered).toBe(true); @@ -550,11 +566,11 @@ describe('RealUnitService', () => { expect(status.userData!.name).toBe('Signed Name'); }); - it('returns state=ADD_WALLET when a step exists for a different wallet but not the current one', () => { + it('returns state=ADD_WALLET when a step exists for a different wallet but not the current one', async () => { const userData = buildVerifiedUserData(); userData.getStepsWith.mockReturnValue([buildStepForWallet(otherWalletAddress, { isCompleted: true })]); - const status = service.getRegistrationInfo(userData, walletAddress); + const status = await service.getRegistrationInfo(userData, walletAddress); expect(status.state).toBe(RealUnitRegistrationState.ADD_WALLET); expect(status.isRegistered).toBe(false); @@ -564,10 +580,10 @@ describe('RealUnitService', () => { expect(status.userData!.name).toBe('Signed Name'); }); - it('returns state=NEW_REGISTRATION when no step exists but userData has firstname/surname', () => { + it('returns state=NEW_REGISTRATION when no step exists but userData has firstname/surname', async () => { const userData = buildVerifiedUserData(); - const status = service.getRegistrationInfo(userData, walletAddress); + const status = await service.getRegistrationInfo(userData, walletAddress); expect(status.state).toBe(RealUnitRegistrationState.NEW_REGISTRATION); expect(status.isRegistered).toBe(false); @@ -587,36 +603,36 @@ describe('RealUnitService', () => { expect(status.userData!.kycData.lastName).toBe('Mustermann'); }); - it('returns state=NEW_REGISTRATION with no userData when no step exists and no KYC data is present (first-time user gets an empty form)', () => { + it('returns state=NEW_REGISTRATION with no userData when no step exists and no KYC data is present (first-time user gets an empty form)', async () => { const userData = { firstname: null, surname: null, getStepsWith: jest.fn().mockReturnValue([]), } as any; - const status = service.getRegistrationInfo(userData, walletAddress); + const status = await service.getRegistrationInfo(userData, walletAddress); expect(status.state).toBe(RealUnitRegistrationState.NEW_REGISTRATION); expect(status.isRegistered).toBe(false); expect(status.userData).toBeUndefined(); }); - it('defaults swissTaxResidence to false in NEW_REGISTRATION when the residence country is not CH', () => { + it('defaults swissTaxResidence to false in NEW_REGISTRATION when the residence country is not CH', async () => { const userData = buildVerifiedUserData(); userData.country = { id: 2, symbol: 'DE' }; - const status = service.getRegistrationInfo(userData, walletAddress); + const status = await service.getRegistrationInfo(userData, walletAddress); expect(status.state).toBe(RealUnitRegistrationState.NEW_REGISTRATION); expect(status.userData!.swissTaxResidence).toBe(false); expect(status.userData!.addressCountry).toBe('DE'); }); - it('falls back to EN in NEW_REGISTRATION when the user language is not one of the RealUnit-supported codes', () => { + it('falls back to EN in NEW_REGISTRATION when the user language is not one of the RealUnit-supported codes', async () => { const userData = buildVerifiedUserData(); userData.language = { symbol: 'ES' }; - const status = service.getRegistrationInfo(userData, walletAddress); + const status = await service.getRegistrationInfo(userData, walletAddress); expect(status.state).toBe(RealUnitRegistrationState.NEW_REGISTRATION); expect(status.userData!.lang).toBe('EN'); diff --git a/src/subdomains/supporting/realunit/dto/realunit-registration.dto.ts b/src/subdomains/supporting/realunit/dto/realunit-registration.dto.ts index daf717f1ca..c1920ed615 100644 --- a/src/subdomains/supporting/realunit/dto/realunit-registration.dto.ts +++ b/src/subdomains/supporting/realunit/dto/realunit-registration.dto.ts @@ -240,6 +240,7 @@ export enum RealUnitRegistrationState { ALREADY_REGISTERED = 'AlreadyRegistered', ADD_WALLET = 'AddWallet', NEW_REGISTRATION = 'NewRegistration', + MERGE_PROCESSING = 'MergeProcessing', } export class RealUnitRegistrationInfoDto { @@ -253,7 +254,7 @@ export class RealUnitRegistrationInfoDto { @ApiProperty({ enum: RealUnitRegistrationState, description: - 'Action the client should take for this wallet. `AlreadyRegistered`: no UX needed. `AddWallet`: render a one-tap Add-Wallet flow that submits to POST /register/wallet using the prior signed payload (`userData` is set). `NewRegistration`: render the full registration form — pre-filled with `userData` when present, otherwise empty for the client to collect every field manually.', + 'Action the client should take for this wallet. `AlreadyRegistered`: no UX needed. `AddWallet`: render a one-tap Add-Wallet flow that submits to POST /register/wallet using the prior signed payload (`userData` is set). `NewRegistration`: render the full registration form — pre-filled with `userData` when present, otherwise empty for the client to collect every field manually. `MergeProcessing`: an account merge for this user is still propagating — render a waiting state and poll; do not treat the absent `userData` as a failure.', }) state: RealUnitRegistrationState; diff --git a/src/subdomains/supporting/realunit/realunit.service.ts b/src/subdomains/supporting/realunit/realunit.service.ts index de4390e88b..497dfcf22f 100644 --- a/src/subdomains/supporting/realunit/realunit.service.ts +++ b/src/subdomains/supporting/realunit/realunit.service.ts @@ -708,7 +708,14 @@ export class RealUnitService { // --- Wallet Methods --- - getRegistrationInfo(userData: UserData, walletAddress: string): RealUnitRegistrationInfoDto { + async getRegistrationInfo(userData: UserData, walletAddress: string): Promise { + // While an account merge for this user is still propagating, the merged-in KYC steps and personal + // data are not yet re-parented, so the branches below would misreport NEW_REGISTRATION with an + // empty `userData`. Signal MERGE_PROCESSING instead so the client renders a waiting state and polls + // until propagation completes — mirrors the KYC path (KycService #toDto via hasProcessingMerge). + if (await this.accountMergeService.hasProcessingMerge(userData.id)) + return { isRegistered: false, state: RealUnitRegistrationState.MERGE_PROCESSING }; + const { step, isForCurrentWallet } = this.findRegistrationStep(userData, walletAddress); // Dispatch to one of three states so the client can route to the right UX without inferring