diff --git a/kyc-nest/src/app.module.ts b/kyc-nest/src/app.module.ts index c3dc8a1..4563b82 100644 --- a/kyc-nest/src/app.module.ts +++ b/kyc-nest/src/app.module.ts @@ -3,6 +3,7 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ProfileModule } from './profile/profile.module'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { KycModule } from './kyc/kyc.module'; @Module({ imports: [ @@ -13,6 +14,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; synchronize: true, }), ProfileModule, + KycModule, ], controllers: [AppController], providers: [AppService], diff --git a/kyc-nest/src/kyc/kyc.controller.ts b/kyc-nest/src/kyc/kyc.controller.ts new file mode 100644 index 0000000..c79b582 --- /dev/null +++ b/kyc-nest/src/kyc/kyc.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Post, Body } from '@nestjs/common'; +import { KYCService } from './kyc.service'; +import { WebhookMessage } from './webhook-message.interface'; + +@Controller('kyc') +export class KYCController { + constructor(private readonly kycService: KYCService) {} + + @Post('webhook') + async handleWebhook(@Body() message: WebhookMessage): Promise { + await this.kycService.handleWebhook(message); + } +} diff --git a/kyc-nest/src/kyc/kyc.module.ts b/kyc-nest/src/kyc/kyc.module.ts new file mode 100644 index 0000000..03e4140 --- /dev/null +++ b/kyc-nest/src/kyc/kyc.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { KYCService } from './kyc.service'; +import { KYCController } from './kyc.controller'; +import { ProfileModule } from 'src/profile/profile.module'; + +@Module({ + imports: [ProfileModule], + providers: [KYCService], + controllers: [KYCController], +}) +export class KycModule {} diff --git a/kyc-nest/src/kyc/kyc.service.ts b/kyc-nest/src/kyc/kyc.service.ts new file mode 100644 index 0000000..1467aa1 --- /dev/null +++ b/kyc-nest/src/kyc/kyc.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { WebhookMessage } from './webhook-message.interface'; +import { ProfileService } from '../profile/profile.service'; + +@Injectable() +export class KYCService { + constructor(private readonly profileService: ProfileService) {} + + async handleWebhook(message: WebhookMessage): Promise { + // In a real application, you should verify a signature of the message here + const userId = message.userId; + + console.log('Received webhook message:', message); + + if (message.result === 'success') { + await this.profileService.update(userId, { + kycStatus: 'KYCIDVerified', + idVerificationId: message.verificationId, + idVerifiedAt: message.timestamp, + }); + } else if (message.result === 'rejected') { + await this.profileService.update(userId, { + kycStatus: 'KYCIDRejected', + idVerificationId: message.verificationId, + idRejectedAt: message.timestamp, + }); + } else { + console.error('Unknown ID verification result:', message.result); + } + } +} diff --git a/kyc-nest/src/kyc/webhook-message.interface.ts b/kyc-nest/src/kyc/webhook-message.interface.ts new file mode 100644 index 0000000..02beb03 --- /dev/null +++ b/kyc-nest/src/kyc/webhook-message.interface.ts @@ -0,0 +1,30 @@ +/** + * Webhook message interface that is sent by the ID verification service. This service is simulated + * in this project, so it lacks a signature which is normally used to verify the authenticity of + * the request. + * + * Some examples of messages are: + * + * Successful verification: + * + * { + * "userId": "5f9f1b9b-7b1e-4b9f-9f1b-9b7b1e4b9f9f", + * "result": "success", + * "verificationId": "5f9f1b9b-7b1e-4b9f-9f1b-9b7b1e4b9f9f", + * "timestamp": "2020-01-01T00:00:00.000Z" + * } + * + * Rejected verification: + * { + * "userId": "5f9f1b9b-7b1e-4b9f-9f1b-9b7b1e4b9f9f", + * "result": "rejected", + * "verificationId": "5f9f1b9b-7b1e-4b9f-9f1b-9b7b1e4b9f9f", + * "timestamp": "2020-01-01T00:00:00.000Z" + * } + */ +export interface WebhookMessage { + userId: string; + result: 'success' | 'rejected'; + verificationId: string; + timestamp: string; +} diff --git a/kyc-nest/src/profile/profile.entity.ts b/kyc-nest/src/profile/profile.entity.ts index d98c26a..1b80f7a 100644 --- a/kyc-nest/src/profile/profile.entity.ts +++ b/kyc-nest/src/profile/profile.entity.ts @@ -1,6 +1,6 @@ import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; -export type KYCStatus = 'KYCPending'; +export type KYCStatus = 'KYCPending' | 'KYCIDVerified' | 'KYCIDRejected'; @Entity() export class Profile { @@ -42,4 +42,13 @@ export class Profile { @Column({ default: 'KYCPending' }) kycStatus: KYCStatus; + + @Column({ nullable: true }) + idVerificationId?: string; + + @Column({ nullable: true }) + idVerifiedAt?: string; + + @Column({ nullable: true }) + idRejectedAt?: string; } diff --git a/kyc-nest/src/profile/profile.module.ts b/kyc-nest/src/profile/profile.module.ts index 3104c25..7ccb533 100644 --- a/kyc-nest/src/profile/profile.module.ts +++ b/kyc-nest/src/profile/profile.module.ts @@ -8,5 +8,6 @@ import { Profile } from './profile.entity'; imports: [TypeOrmModule.forFeature([Profile])], controllers: [ProfileController], providers: [ProfileService], + exports: [ProfileService], }) export class ProfileModule {} diff --git a/kyc-nest/src/profile/profile.service.ts b/kyc-nest/src/profile/profile.service.ts index 95c4573..37fb791 100644 --- a/kyc-nest/src/profile/profile.service.ts +++ b/kyc-nest/src/profile/profile.service.ts @@ -20,6 +20,21 @@ export class ProfileService { return newProfile; } + async update(userId: string, profileData: Partial): Promise { + const profile = await this.findById(userId); + + if ( + profileData.kycStatus && + !this.isValidTransition(profile.kycStatus, profileData.kycStatus) + ) { + throw new BadRequestException( + `Invalid status transition from '${profile.kycStatus}' to '${profileData.kycStatus}'`, + ); + } + + await this.profileRepository.update(userId, profileData); + } + async findAll(): Promise { return this.profileRepository.find(); } @@ -35,4 +50,17 @@ export class ProfileService { } return profile; } + + private isValidTransition( + currentState: KYCStatus, + newState: KYCStatus, + ): boolean { + const allowedTransitions: Record> = { + KYCPending: ['KYCIDVerified', 'KYCIDRejected'], + KYCIDVerified: [], + KYCIDRejected: [], + }; + + return allowedTransitions[currentState]?.includes(newState); + } }