Skip to content

Commit 4dfe78d

Browse files
authored
feat: split RealUnit register endpoint (#3057)
* feat: add register/email and register/complete endpoints. * chore: RegistrationStatus naming. * chore: remove duplicated code. * fix: check for merge string.
1 parent 632fe79 commit 4dfe78d

3 files changed

Lines changed: 168 additions & 2 deletions

File tree

src/subdomains/supporting/realunit/controllers/realunit.controller.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ApiAcceptedResponse,
1717
ApiBadRequestResponse,
1818
ApiBearerAuth,
19+
ApiConflictResponse,
1920
ApiExcludeEndpoint,
2021
ApiOkResponse,
2122
ApiOperation,
@@ -50,6 +51,8 @@ import {
5051
RealUnitSingleReceiptPdfDto,
5152
} from '../dto/realunit-pdf.dto';
5253
import {
54+
RealUnitEmailRegistrationDto,
55+
RealUnitEmailRegistrationResponseDto,
5356
RealUnitRegistrationDto,
5457
RealUnitRegistrationResponseDto,
5558
RealUnitRegistrationStatus,
@@ -319,7 +322,55 @@ export class RealUnitController {
319322
return this.realunitService.confirmSell(jwt.user, +id, dto);
320323
}
321324

322-
// --- Registration Endpoint ---
325+
// --- Registration Endpoints ---
326+
327+
@Post('register/email')
328+
@ApiBearerAuth()
329+
@UseGuards(AuthGuard(), RoleGuard(UserRole.ACCOUNT), UserActiveGuard())
330+
@ApiOperation({
331+
summary: 'Step 1: Register email for RealUnit',
332+
description:
333+
'First step of RealUnit registration. Checks if email exists in DFX system. If exists and merge is possible, sends merge confirmation email. Otherwise registers email and sets KYC Level 10.',
334+
})
335+
@ApiOkResponse({ type: RealUnitEmailRegistrationResponseDto })
336+
@ApiBadRequestResponse({ description: 'Email does not match verified email' })
337+
@ApiConflictResponse({ description: 'Account already exists and merge not possible' })
338+
async registerEmail(
339+
@GetJwt() jwt: JwtPayload,
340+
@Body() dto: RealUnitEmailRegistrationDto,
341+
): Promise<RealUnitEmailRegistrationResponseDto> {
342+
const status = await this.realunitService.registerEmail(jwt.account, dto);
343+
return { status };
344+
}
345+
346+
@Post('register/complete')
347+
@ApiBearerAuth()
348+
@UseGuards(AuthGuard(), RoleGuard(UserRole.ACCOUNT), UserActiveGuard())
349+
@ApiOperation({
350+
summary: 'Step 2: Complete RealUnit registration',
351+
description:
352+
'Second step of RealUnit registration. Requires email registration to be completed. Validates personal data against DFX system and forwards to Aktionariat.',
353+
})
354+
@ApiOkResponse({ type: RealUnitRegistrationResponseDto })
355+
@ApiAcceptedResponse({
356+
type: RealUnitRegistrationResponseDto,
357+
description: 'Registration accepted, manual review needed or forwarding to Aktionariat failed',
358+
})
359+
@ApiBadRequestResponse({
360+
description: 'Invalid signature, wallet mismatch, email registration not completed, or data mismatch',
361+
})
362+
async completeRegistration(
363+
@GetJwt() jwt: JwtPayload,
364+
@Body() dto: RealUnitRegistrationDto,
365+
@Res() res: Response,
366+
): Promise<void> {
367+
const status = await this.realunitService.completeRegistration(jwt.account, dto);
368+
const response: RealUnitRegistrationResponseDto = {
369+
status: status,
370+
};
371+
const statusCode = status === RealUnitRegistrationStatus.COMPLETED ? HttpStatus.CREATED : HttpStatus.ACCEPTED;
372+
res.status(statusCode).json(response);
373+
}
323374

324375
@Post('register')
325376
@ApiBearerAuth()

src/subdomains/supporting/realunit/dto/realunit-registration.dto.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,34 @@ export enum RealUnitUserType {
2626
export enum RealUnitRegistrationStatus {
2727
COMPLETED = 'completed',
2828
PENDING_REVIEW = 'pending_review',
29+
MANUAL_REVIEW_DATA_MISMATCH = 'manual_review_data_mismatch',
30+
FORWARDING_FAILED = 'forwarding_failed',
2931
}
3032

3133
export class RealUnitRegistrationResponseDto {
3234
@ApiProperty({ enum: RealUnitRegistrationStatus })
3335
status: RealUnitRegistrationStatus;
3436
}
3537

38+
export enum RealUnitEmailRegistrationStatus {
39+
EMAIL_REGISTERED = 'email_registered',
40+
MERGE_REQUESTED = 'merge_requested',
41+
}
42+
43+
export class RealUnitEmailRegistrationDto {
44+
@ApiProperty()
45+
@IsNotEmpty()
46+
@IsEmail()
47+
@IsLowercase()
48+
@Transform(Util.trim)
49+
email: string;
50+
}
51+
52+
export class RealUnitEmailRegistrationResponseDto {
53+
@ApiProperty({ enum: RealUnitEmailRegistrationStatus })
54+
status: RealUnitEmailRegistrationStatus;
55+
}
56+
3657
export enum RealUnitLanguage {
3758
EN = 'EN',
3859
DE = 'DE',

src/subdomains/supporting/realunit/realunit.service.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { KycStep } from 'src/subdomains/generic/kyc/entities/kyc-step.entity';
3434
import { KycStepName } from 'src/subdomains/generic/kyc/enums/kyc-step-name.enum';
3535
import { ReviewStatus } from 'src/subdomains/generic/kyc/enums/review-status.enum';
3636
import { KycService } from 'src/subdomains/generic/kyc/services/kyc.service';
37+
import { AccountMergeService } from 'src/subdomains/generic/user/models/account-merge/account-merge.service';
3738
import { AccountType } from 'src/subdomains/generic/user/models/user-data/account-type.enum';
3839
import { UserData } from 'src/subdomains/generic/user/models/user-data/user-data.entity';
3940
import { KycLevel } from 'src/subdomains/generic/user/models/user-data/user-data.enum';
@@ -52,7 +53,14 @@ import {
5253
TokenInfoClientResponse,
5354
} from './dto/client.dto';
5455
import { RealUnitDtoMapper } from './dto/realunit-dto.mapper';
55-
import { AktionariatRegistrationDto, RealUnitRegistrationDto, RealUnitUserType } from './dto/realunit-registration.dto';
56+
import {
57+
AktionariatRegistrationDto,
58+
RealUnitEmailRegistrationDto,
59+
RealUnitEmailRegistrationStatus,
60+
RealUnitRegistrationDto,
61+
RealUnitRegistrationStatus,
62+
RealUnitUserType,
63+
} from './dto/realunit-registration.dto';
5664
import { RealUnitSellConfirmDto, RealUnitSellDto, RealUnitSellPaymentInfoDto } from './dto/realunit-sell.dto';
5765
import {
5866
AccountHistoryDto,
@@ -98,6 +106,7 @@ export class RealUnitService {
98106
private readonly sellService: SellService,
99107
private readonly eip7702DelegationService: Eip7702DelegationService,
100108
private readonly transactionRequestService: TransactionRequestService,
109+
private readonly accountMergeService: AccountMergeService,
101110
) {
102111
this.ponderUrl = GetConfig().blockchain.realunit.graphUrl;
103112
}
@@ -341,6 +350,91 @@ export class RealUnitService {
341350
return !success;
342351
}
343352

353+
async registerEmail(userDataId: number, dto: RealUnitEmailRegistrationDto): Promise<RealUnitEmailRegistrationStatus> {
354+
const userData = await this.userDataService.getUserData(userDataId, { users: true });
355+
if (!userData) throw new NotFoundException('User not found');
356+
357+
if (!userData.mail) {
358+
try {
359+
await this.userDataService.trySetUserMail(userData, dto.email);
360+
} catch (e) {
361+
if (e instanceof ConflictException) {
362+
if (e.message.includes('account merge request sent')) {
363+
return RealUnitEmailRegistrationStatus.MERGE_REQUESTED;
364+
}
365+
}
366+
throw e;
367+
}
368+
} else if (!Util.equalsIgnoreCase(dto.email, userData.mail)) {
369+
throw new BadRequestException('Email does not match verified email');
370+
}
371+
372+
if (userData.kycLevel < KycLevel.LEVEL_10) {
373+
await this.kycService.initializeProcess(userData);
374+
}
375+
376+
return RealUnitEmailRegistrationStatus.EMAIL_REGISTERED;
377+
}
378+
379+
async completeRegistration(userDataId: number, dto: RealUnitRegistrationDto): Promise<RealUnitRegistrationStatus> {
380+
await this.validateRegistrationDto(dto);
381+
382+
// get and validate user
383+
const userData = await this.userService
384+
.getUserByAddress(dto.walletAddress, {
385+
userData: { kycSteps: true, users: true, country: true, organizationCountry: true },
386+
})
387+
.then((u) => u?.userData);
388+
389+
if (!userData) throw new NotFoundException('User not found');
390+
if (userData.id !== userDataId) throw new BadRequestException('Wallet address does not belong to user');
391+
392+
if (userData.kycLevel < KycLevel.LEVEL_10 || !userData.mail) {
393+
throw new BadRequestException('Email registration must be completed first');
394+
}
395+
if (!Util.equalsIgnoreCase(dto.email, userData.mail)) {
396+
throw new BadRequestException('Email does not match registered email');
397+
}
398+
399+
// duplicate check
400+
if (userData.getNonFailedStepWith(KycStepName.REALUNIT_REGISTRATION)) {
401+
throw new BadRequestException('RealUnit registration already exists');
402+
}
403+
404+
// store data with internal review
405+
const kycStep = await this.kycService.createCustomKycStep(
406+
userData,
407+
KycStepName.REALUNIT_REGISTRATION,
408+
ReviewStatus.INTERNAL_REVIEW,
409+
dto,
410+
);
411+
412+
const hasExistingData = userData.firstname != null;
413+
if (hasExistingData) {
414+
const dataMatches = this.isPersonalDataMatching(userData, dto);
415+
if (!dataMatches) {
416+
await this.kycService.saveKycStepUpdate(kycStep.manualReview('Existing KYC data does not match'));
417+
return RealUnitRegistrationStatus.MANUAL_REVIEW_DATA_MISMATCH;
418+
}
419+
} else {
420+
await this.userDataService.updatePersonalData(userData, dto.kycData);
421+
}
422+
423+
// forward to Aktionariat
424+
const success = await this.forwardRegistration(kycStep, dto);
425+
if (!success) return RealUnitRegistrationStatus.FORWARDING_FAILED;
426+
427+
// only update after successful forward
428+
await this.userDataService.updateUserDataInternal(userData, {
429+
nationality: await this.countryService.getCountryWithSymbol(dto.nationality),
430+
birthday: new Date(dto.birthday),
431+
language: dto.lang && (await this.languageService.getLanguageBySymbol(dto.lang)),
432+
tin: dto.countryAndTINs?.length ? JSON.stringify(dto.countryAndTINs) : undefined,
433+
});
434+
435+
return RealUnitRegistrationStatus.COMPLETED;
436+
}
437+
344438
private async validateRegistrationDto(dto: RealUnitRegistrationDto): Promise<void> {
345439
// signature validation
346440
if (!this.verifyRealUnitRegistrationSignature(dto)) {

0 commit comments

Comments
 (0)