From c2f6607bcde527c6735ab6287ca1e777c9def726 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:02:41 +0100 Subject: [PATCH] fix: review nationality step immediately instead of waiting for cron (#3226) * fix: review nationality step immediately instead of waiting for cron job The nationality review check is trivial (blocked/merged user, allowed nationality) and can be performed inline when the user submits their data, eliminating the up-to-1-minute wait for the cron job to process the step. * refactor: extract shared reviewNationalityData helper method Eliminate code duplication between updateNationalityStep (inline review) and reviewNationalityStep (cron job) by extracting the review logic into a single private helper method. * refactor: remove dead nationality branch from updateKycStep The nationality path is now handled by the dedicated updateNationalityStep method, so the if (data.nationality) branch in updateKycStep is unreachable. * refactor: save nationality data with final status in single DB write Address review feedback: instead of first saving with INTERNAL_REVIEW and then immediately changing the status, determine the final status inline and save data + status in one DB write. This eliminates the unnecessary intermediate INTERNAL_REVIEW state and reduces DB writes and step logs from 2 to 1 for non-residence-permit countries. --- .../generic/kyc/controllers/kyc.controller.ts | 2 +- .../generic/kyc/services/kyc.service.ts | 84 +++++++++++++------ 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/src/subdomains/generic/kyc/controllers/kyc.controller.ts b/src/subdomains/generic/kyc/controllers/kyc.controller.ts index 36b92ea510..c0c7aa4332 100644 --- a/src/subdomains/generic/kyc/controllers/kyc.controller.ts +++ b/src/subdomains/generic/kyc/controllers/kyc.controller.ts @@ -214,7 +214,7 @@ export class KycController { @Param('id') id: string, @Body() data: KycNationalityData, ): Promise { - return this.kycService.updateKycStep(code, +id, data, ReviewStatus.INTERNAL_REVIEW); + return this.kycService.updateNationalityStep(code, +id, data); } @Put('data/recommendation/:id') diff --git a/src/subdomains/generic/kyc/services/kyc.service.ts b/src/subdomains/generic/kyc/services/kyc.service.ts index b5024508bb..958d808b70 100644 --- a/src/subdomains/generic/kyc/services/kyc.service.ts +++ b/src/subdomains/generic/kyc/services/kyc.service.ts @@ -178,22 +178,7 @@ export class KycService { const result = entity.getResult(); const nationality = await this.countryService.getCountry(result.nationality.id); - //Skip nationalities which needs a residencePermit first - if (Config.kyc.residencePermitCountries.includes(nationality.symbol)) continue; - - const errors = this.getNationalityErrors(entity, nationality); - const comment = errors.join(';'); - - if (errors.some((e) => KycStepIgnoringErrors.includes(e))) { - await this.kycStepRepo.update(...entity.ignored(comment)); - } else if (errors.length > 0) { - await this.kycStepRepo.update(...entity.manualReview(comment)); - } else { - await this.kycStepRepo.update(...entity.complete()); - await this.checkDfxApproval(entity); - } - - await this.createStepLog(entity.userData, entity); + await this.reviewNationalityData(entity, entity.userData, nationality); } catch (e) { this.logger.error(`Failed to auto review nationality step ${entity.id}:`, e); } @@ -586,21 +571,30 @@ export class KycService { data: Partial, reviewStatus: ReviewStatus, ): Promise { - let user = await this.getUser(kycHash); + const user = await this.getUser(kycHash); const kycStep = user.getPendingStepOrThrow(stepId); - if (data.nationality) { - const nationality = await this.countryService.getCountry(data.nationality.id); - if (!nationality) throw new BadRequestException('Nationality not found'); - - Object.assign(data.nationality, { id: nationality.id, symbol: nationality.symbol }); - } else { - user = await this.userDataService.updateUserDataInternal(user, data); - } + await this.userDataService.updateUserDataInternal(user, data); return this.updateKycStepAndLog(kycStep, user, data, reviewStatus); } + async updateNationalityStep(kycHash: string, stepId: number, data: KycNationalityData): Promise { + const user = await this.getUser(kycHash); + const kycStep = user.getPendingStepOrThrow(stepId); + + const nationality = await this.countryService.getCountry(data.nationality.id); + if (!nationality) throw new BadRequestException('Nationality not found'); + + Object.assign(data.nationality, { id: nationality.id, symbol: nationality.symbol }); + + await this.reviewNationalityData(kycStep, user, nationality, data); + + await this.updateProgress(user, false); + + return KycStepMapper.toStepBase(kycStep); + } + async updateBeneficialOwnerData(kycHash: string, stepId: number, data: KycBeneficialData): Promise { const user = await this.getUser(kycHash); const kycStep = user.getPendingStepOrThrow(stepId); @@ -1416,6 +1410,46 @@ export class KycService { return errors; } + private async reviewNationalityData( + kycStep: KycStep, + user: UserData, + nationality: Country, + data?: KycStepResult, + ): Promise { + if (Config.kyc.residencePermitCountries.includes(nationality.symbol)) { + if (data) { + await this.kycStepRepo.update(...kycStep.update(ReviewStatus.INTERNAL_REVIEW, data)); + await this.createStepLog(user, kycStep); + } + return; + } + + const errors = this.getNationalityErrors(kycStep, nationality); + const comment = errors.join(';'); + + if (data) { + if (errors.some((e) => KycStepIgnoringErrors.includes(e))) { + await this.kycStepRepo.update(...kycStep.update(ReviewStatus.IGNORED, data, comment)); + } else if (errors.length > 0) { + await this.kycStepRepo.update(...kycStep.update(ReviewStatus.MANUAL_REVIEW, data, comment)); + } else { + await this.kycStepRepo.update(...kycStep.update(ReviewStatus.COMPLETED, data)); + await this.checkDfxApproval(kycStep); + } + } else { + if (errors.some((e) => KycStepIgnoringErrors.includes(e))) { + await this.kycStepRepo.update(...kycStep.ignored(comment)); + } else if (errors.length > 0) { + await this.kycStepRepo.update(...kycStep.manualReview(comment)); + } else { + await this.kycStepRepo.update(...kycStep.complete()); + await this.checkDfxApproval(kycStep); + } + } + + await this.createStepLog(user, kycStep); + } + private getFinancialDataErrors(entity: KycStep): KycError[] { const errors = this.getStepDefaultErrors(entity); const financialStepResult = entity.getResult();