Skip to content

Commit

Permalink
[DEV-3439] add video sumsub (#1860)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kolibri1990 authored Jan 6, 2025
1 parent 301a18c commit 09c84e9
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 17 deletions.
3 changes: 2 additions & 1 deletion src/subdomains/generic/kyc/dto/mapper/kyc-info.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ export class KycInfoMapper {
private static sortSteps(steps: KycStep[]): KycStep[] {
const completedIdentSteps = steps.filter((s) => s.name === KycStepName.IDENT && s.isCompleted);
const fullIdentStep =
completedIdentSteps.find((s) => s.type === KycStepType.MANUAL) ?? steps.find((s) => s.type === KycStepType.VIDEO);
completedIdentSteps.find((s) => s.type === KycStepType.MANUAL) ??
steps.find((s) => s.type === KycStepType.VIDEO || s.type === KycStepType.SUMSUB_VIDEO);

const groupedSteps = steps
// hide all other ident steps, if full ident is completed
Expand Down
28 changes: 25 additions & 3 deletions src/subdomains/generic/kyc/dto/sum-sub.dto.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BadRequest } from 'ccxt';
import { IdentShortResult } from './ident-result.dto';

export interface SumsubResult {
Expand Down Expand Up @@ -94,6 +95,11 @@ export enum ReviewRejectType {
RETRY = 'RETRY',
}

export enum VideoIdentStatus {
PENDING = 'pending',
COMPLETED = 'completed',
}

export enum SumSubWebhookType {
APPLICANT_CREATED = 'applicantCreated',
APPLICANT_PENDING = 'applicantPending',
Expand Down Expand Up @@ -263,9 +269,25 @@ const SumSubReasonMap: Record<SumSubRejectionLabels, string> = {
};

export function getSumsubResult(dto: SumSubWebhookResult): IdentShortResult {
if (dto.type === SumSubWebhookType.APPLICANT_PENDING) return IdentShortResult.REVIEW;
if (dto.type === SumSubWebhookType.APPLICANT_REVIEWED)
return dto.reviewResult.reviewAnswer === ReviewAnswer.GREEN ? IdentShortResult.SUCCESS : IdentShortResult.FAIL;
switch (dto.type) {
case SumSubWebhookType.APPLICANT_PENDING:
return IdentShortResult.REVIEW;

case SumSubWebhookType.APPLICANT_REVIEWED:
return dto.reviewResult.reviewAnswer === ReviewAnswer.GREEN ? IdentShortResult.SUCCESS : IdentShortResult.FAIL;

case SumSubWebhookType.VIDEO_IDENT_STATUS_CHANGED:
if (dto.videoIdentReviewStatus === VideoIdentStatus.PENDING) {
return IdentShortResult.REVIEW;
}
if (dto.videoIdentReviewStatus === VideoIdentStatus.COMPLETED) {
return dto.reviewResult.reviewAnswer === ReviewAnswer.GREEN ? IdentShortResult.SUCCESS : IdentShortResult.FAIL;
}
break;

default:
throw new BadRequest(`Unknown webhook type: ${dto.type}`);
}
}

export function getSumSubReason(reasons: SumSubRejectionLabels[]): string {
Expand Down
2 changes: 1 addition & 1 deletion src/subdomains/generic/kyc/entities/kyc-step.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ export class KycStep extends IEntity {
}

get isSumsub(): boolean {
return this.type === KycStepType.SUMSUB_AUTO;
return this.type === KycStepType.SUMSUB_AUTO || this.type === KycStepType.SUMSUB_VIDEO;
}

get isManual(): boolean {
Expand Down
1 change: 1 addition & 0 deletions src/subdomains/generic/kyc/enums/kyc.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export enum KycStepType {
VIDEO = 'Video',
MANUAL = 'Manual',
SUMSUB_AUTO = 'SumsubAuto',
SUMSUB_VIDEO = 'SumsubVideo',
}

export enum KycLogType {
Expand Down
28 changes: 21 additions & 7 deletions src/subdomains/generic/kyc/services/integration/sum-sub.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import { IdentDocument } from '../../dto/ident.dto';
import { ApplicantType, SumSubDataResult, SumsubResult } from '../../dto/sum-sub.dto';
import { KycStep } from '../../entities/kyc-step.entity';
import { ContentType } from '../../enums/content-type.enum';
import { KycStepType } from '../../enums/kyc.enum';

@Injectable()
export class SumsubService {
private readonly logger = new DfxLogger(SumsubService);

private readonly baseUrl = `https://api.sumsub.com`;
private readonly kycLevel = 'CH-Standard';
private readonly kycLevelAuto = 'CH-Standard';
private readonly kycLevelVideo = 'CH-Standard-Video';

private static readonly algoMap: { [key: string]: string } = {
HMAC_SHA1_HEX: 'sha1',
HMAC_SHA256_HEX: 'sha256',
Expand All @@ -29,8 +32,8 @@ export class SumsubService {
async initiateIdent(user: UserData, kycStep: KycStep): Promise<string> {
if (!kycStep.transactionId) throw new InternalServerErrorException('Transaction ID is missing');

await this.createApplicant(kycStep.transactionId, user);
return this.generateAccessToken(kycStep.transactionId).then((r) => r.token);
await this.createApplicant(kycStep.transactionId, user, kycStep.type);
return this.generateAccessToken(kycStep.transactionId, kycStep.type).then((r) => r.token);
}

async getDocuments(kycStep: KycStep): Promise<IdentDocument[]> {
Expand Down Expand Up @@ -74,7 +77,7 @@ export class SumsubService {
}

// --- HELPER METHODS --- //
private async createApplicant(transactionId: string, user: UserData): Promise<void> {
private async createApplicant(transactionId: string, user: UserData, kycStepType: KycStepType): Promise<void> {
const data = {
externalUserId: transactionId,
type: ApplicantType.INDIVIDUAL,
Expand All @@ -84,6 +87,7 @@ export class SumsubService {
country: user.country?.symbol3,
dob: user.birthday && Util.isoDate(user.birthday),
nationality: user.nationality?.symbol3,

addresses: [
{
street: user.street + (user.houseNumber ? ` ${user.houseNumber}` : ''),
Expand All @@ -94,14 +98,24 @@ export class SumsubService {
],
},
};
await this.callApi<{ id: string }>(`/resources/applicants?levelName=${this.kycLevel}`, 'POST', data);

await this.callApi<{ id: string }>(
`/resources/applicants?levelName=${
kycStepType === KycStepType.SUMSUB_AUTO ? this.kycLevelAuto : this.kycLevelVideo
}`,
'POST',
data,
);
}

private async generateAccessToken(transactionId: string): Promise<{ token: string }> {
private async generateAccessToken(transactionId: string, kycStepType: KycStepType): Promise<{ token: string }> {
const expirySecs = Config.kyc.identFailAfterDays * 24 * 60 * 60;
return this.callApi<{ token: string }>(
`/resources/accessTokens?userId=${transactionId}&levelName=${this.kycLevel}&ttlInSecs=${expirySecs}`,
`/resources/accessTokens?userId=${transactionId}&levelName=${
kycStepType === KycStepType.SUMSUB_AUTO ? this.kycLevelAuto : this.kycLevelVideo
}&ttlInSecs=${expirySecs}`,
'POST',
undefined,
);
}

Expand Down
7 changes: 4 additions & 3 deletions src/subdomains/generic/kyc/services/kyc.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,7 @@ export class KycService {

async syncIdentStep(kycStep: KycStep): Promise<void> {
if (!kycStep.isInReview) throw new BadRequestException(`Invalid KYC step status ${kycStep.status}`);
if (kycStep.type === KycStepType.SUMSUB_AUTO)
throw new BadRequestException('Ident step sync is only available for IDnow');
if (kycStep.isSumsub) throw new BadRequestException('Ident step sync is only available for IDnow');

const result = await this.identService.getResult(kycStep);
return this.updateIntrumIdent(result);
Expand Down Expand Up @@ -716,7 +715,9 @@ export class KycService {
nextStep: {
name: nextStep,
type:
lastTry?.type === KycStepType.VIDEO ? KycStepType.VIDEO : await this.userDataService.getIdentMethod(user),
lastTry?.type === KycStepType.VIDEO || lastTry?.type === KycStepType.SUMSUB_VIDEO
? lastTry?.type
: await this.userDataService.getIdentMethod(user),
preventDirectEvaluation,
},
};
Expand Down
15 changes: 13 additions & 2 deletions src/subdomains/generic/user/models/user-data/user-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,16 @@ export class UserDataService {
// cancel a pending video ident, if ident is completed
const identCompleted = userData.hasCompletedStep(KycStepName.IDENT);
const pendingVideo = userData.getPendingStepWith(KycStepName.IDENT, KycStepType.VIDEO);
if (identCompleted && pendingVideo) await this.kycAdminService.updateKycStepInternal(pendingVideo.cancel());
const pendingSumSubVideo = userData.getPendingStepWith(KycStepName.IDENT, KycStepType.SUMSUB_VIDEO);

if (identCompleted) {
if (pendingVideo) {
await this.kycAdminService.updateKycStepInternal(pendingVideo.cancel());
}
if (pendingSumSubVideo) {
await this.kycAdminService.updateKycStepInternal(pendingSumSubVideo.cancel());
}
}
}

// If KYC level >= 50 and DFX-approval not complete, complete it.
Expand Down Expand Up @@ -815,7 +824,9 @@ export class UserDataService {
master.amlListReactivatedDate = slave.amlListReactivatedDate;
master.kycFileId = slave.kycFileId;
}
if (slave.kycSteps.some((k) => k.type === KycStepType.VIDEO && k.isCompleted)) {
if (
slave.kycSteps.some((k) => (k.type === KycStepType.VIDEO || k.type === KycStepType.SUMSUB_VIDEO) && k.isCompleted)
) {
master.identificationType = KycIdentificationType.VIDEO_ID;
master.bankTransactionVerification = CheckStatus.UNNECESSARY;
}
Expand Down

0 comments on commit 09c84e9

Please sign in to comment.