feat: kyc process

This commit is contained in:
Abdalhamid Alhamad
2025-08-07 14:23:33 +03:00
parent 275984954e
commit ee7b365527
73 changed files with 388 additions and 7318 deletions

View File

@ -0,0 +1 @@
export * from './numeric-to-iso.mapper';

View File

@ -0,0 +1,11 @@
import { CountriesNumericISO } from '../constants';
import { CountryIso } from '../enums';
// At module top-level
export const NumericToCountryIso: Record<string, CountryIso> = Object.entries(CountriesNumericISO).reduce(
(acc, [isoKey, numeric]) => {
acc[numeric] = isoKey as CountryIso;
return acc;
},
{} as Record<string, CountryIso>,
);

View File

@ -1,2 +1,3 @@
export * from './create-application.mock';
export * from './initiate-kyc.mock';
export * from './inquire-application.mock';

View File

@ -0,0 +1,21 @@
export const INITIATE_KYC_MOCK = {
ResponseHeader: {
Version: '1.0.0',
MsgUid: 'f3a9d4b2-5c7a-4e2f-8121-9c4e5a6b7d8f',
Source: 'ZOD',
ServiceId: 'InitiateKyc',
ReqDateTime: '2025-08-07T14:20:00.000Z',
RspDateTime: '2025-08-07T14:20:00.123Z',
ResponseCode: '000',
ResponseType: 'Success',
ProcessingTime: 123,
ResponseDescription: 'KYC initiation successful',
},
InitiateKycResponseDetails: {
InstitutionCode: '1100',
TransId: '3136fd60-3f89-4d24-a92f-b9c63a53807f',
RandomNumber: '38',
Status: 'WAITING',
ExpiryDateTime: '2025-08-07T14:30:00.000Z',
},
};

View File

@ -0,0 +1,23 @@
export const getKycCallbackMock = (nationalId: string) => {
return {
InstId: '1100',
transId: '3136fd60-3f89-4d24-a92f-b9c63a53807f',
date: '20250807',
time: '150000',
status: 'SUCCESS',
firstName: 'John',
lastName: 'Doe',
dob: '19990107',
nationality: '682',
gender: 'M',
nationalIdExpiry: '20310917',
nationalId,
mobile: '+962798765432',
salaryMin: '500',
salaryMax: '1000',
incomeSource: 'Salary',
professionTitle: 'Software Engineer',
professionType: 'Full-Time',
isPep: 'N',
};
};

View File

@ -5,6 +5,7 @@ import {
AccountCardStatusChangedWebhookRequest,
AccountTransactionWebhookRequest,
CardTransactionWebhookRequest,
KycWebhookRequest,
} from '../dtos/requests';
import { NeoLeapWebhookService } from '../services';
@ -30,4 +31,10 @@ export class NeoLeapWebhooksController {
await this.neoleapWebhookService.handleAccountCardStatusChangedWebhook(body);
return ResponseFactory.data({ message: 'Card status updated successfully', status: 'success' });
}
@Post('kyc')
async handleKycWebhook(@Body() body: KycWebhookRequest) {
await this.neoleapWebhookService.handleKycWebhook(body);
return ResponseFactory.data({ message: 'KYC processed successfully', status: 'success' });
}
}

View File

@ -1,4 +1,5 @@
export * from './account-card-status-changed-webhook.request.dto';
export * from './account-transaction-webhook.request.dto';
export * from './card-transaction-webhook.request.dto';
export * from './kyc-webhook.request.dto';
export * from './update-card-controls.request.dto';

View File

@ -0,0 +1,99 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import { IsString } from 'class-validator';
export class KycWebhookRequest {
@Expose({ name: 'InstId' })
@IsString()
@ApiProperty({ name: 'InstId', example: '1100' })
instId!: string;
@Expose()
@IsString()
@ApiProperty({ example: '3136fd60-3f89-4d24-a92f-b9c63a53807f' })
transId!: string;
@Expose()
@IsString()
@ApiProperty({ example: '20250807' })
date!: string;
@Expose()
@IsString()
@ApiProperty({ example: '150000' })
time!: string;
@Expose()
@IsString()
@ApiProperty({ example: 'SUCCESS' })
status!: string;
@Expose()
@IsString()
@ApiProperty({ example: 'John' })
firstName!: string;
@Expose()
@IsString()
@ApiProperty({ example: 'Doe' })
lastName!: string;
@Expose()
@IsString()
@ApiProperty({ example: '19990107' })
dob!: string;
@Expose()
@IsString()
@ApiProperty({ example: '682' })
nationality!: string;
@Expose()
@IsString()
@ApiProperty({ example: 'M' })
gender!: string;
@Expose()
@IsString()
@ApiProperty({ example: '20310917' })
nationalIdExpiry!: string;
@Expose()
@IsString()
@ApiProperty({ example: '1250820840' })
nationalId!: string;
@Expose()
@IsString()
@ApiProperty({ example: '+962798765432' })
mobile!: string;
@Expose()
@IsString()
@ApiProperty({ example: '500' })
salaryMin!: string;
@Expose()
@IsString()
@ApiProperty({ example: '1000' })
salaryMax!: string;
@Expose()
@IsString()
@ApiProperty({ example: 'Salary' })
incomeSource!: string;
@Expose()
@IsString()
@ApiProperty({ example: 'Software Engineer' })
professionTitle!: string;
@Expose()
@IsString()
@ApiProperty({ example: 'Full-Time' })
professionType!: string;
@Expose()
@IsString()
@ApiProperty({ example: 'N' })
isPep!: string;
}

View File

@ -1,3 +1,4 @@
export * from './create-application.response.dto';
export * from './initiate-kyc.response.dto';
export * from './inquire-application.response';
export * from './update-card-controls.response.dto';

View File

@ -0,0 +1,28 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose, Transform } from 'class-transformer';
export class InitiateKycResponseDto {
@Transform(({ obj }) => obj?.InstitutionCode)
@Expose()
@ApiProperty()
institutionCode!: string;
@Transform(({ obj }) => obj?.TransId)
@Expose()
@ApiProperty()
transId!: string;
@Transform(({ obj }) => obj?.RandomNumber)
@Expose()
@ApiProperty()
randomNumber!: string;
@Transform(({ obj }) => obj?.Status)
@Expose()
@ApiProperty()
status!: string;
@Transform(({ obj }) => obj?.ExpiryDateTime)
@Expose()
@ApiProperty()
expiryDateTime!: string;
}

View File

@ -1,5 +1,5 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { CardModule } from '~/card/card.module';
import { CustomerModule } from '~/customer/customer.module';
import { NeoLeapWebhooksController } from './controllers/neoleap-webhooks.controller';
@ -8,8 +8,9 @@ import { NeoLeapWebhookService } from './services';
import { NeoLeapService } from './services/neoleap.service';
@Module({
imports: [HttpModule, CustomerModule, CardModule],
imports: [HttpModule, CardModule, forwardRef(() => CustomerModule)],
controllers: [NeoTestController, NeoLeapWebhooksController],
providers: [NeoLeapService, NeoLeapWebhookService],
exports: [NeoLeapService],
})
export class NeoLeapModule {}

View File

@ -1,15 +1,21 @@
import { Injectable } from '@nestjs/common';
import { CardService } from '~/card/services';
import { TransactionService } from '~/card/services/transaction.service';
import { CustomerService } from '~/customer/services';
import {
AccountCardStatusChangedWebhookRequest,
AccountTransactionWebhookRequest,
CardTransactionWebhookRequest,
KycWebhookRequest,
} from '../dtos/requests';
@Injectable()
export class NeoLeapWebhookService {
constructor(private readonly transactionService: TransactionService, private readonly cardService: CardService) {}
constructor(
private readonly transactionService: TransactionService,
private readonly cardService: CardService,
private customerService: CustomerService,
) {}
handleCardTransactionWebhook(body: CardTransactionWebhookRequest) {
return this.transactionService.createCardTransaction(body);
@ -22,4 +28,8 @@ export class NeoLeapWebhookService {
handleAccountCardStatusChangedWebhook(body: AccountCardStatusChangedWebhookRequest) {
return this.cardService.updateCardStatus(body);
}
handleKycWebhook(body: KycWebhookRequest) {
return this.customerService.updateCustomerKyc(body);
}
}

View File

@ -1,14 +1,21 @@
import { HttpService } from '@nestjs/axios';
import { BadRequestException, Injectable, InternalServerErrorException } from '@nestjs/common';
import { BadRequestException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ClassConstructor, plainToInstance } from 'class-transformer';
import moment from 'moment';
import { v4 as uuid } from 'uuid';
import { CountriesNumericISO } from '~/common/constants';
import { InitiateKycRequestDto } from '~/customer/dtos/request';
import { Customer } from '~/customer/entities';
import { Gender, KycStatus } from '~/customer/enums';
import { CREATE_APPLICATION_MOCK, INQUIRE_APPLICATION_MOCK } from '../__mocks__/';
import { CreateApplicationResponse, InquireApplicationResponse, UpdateCardControlsResponseDto } from '../dtos/response';
import { CREATE_APPLICATION_MOCK, INITIATE_KYC_MOCK, INQUIRE_APPLICATION_MOCK } from '../__mocks__/';
import { getKycCallbackMock } from '../__mocks__/kyc-callback.mock';
import {
CreateApplicationResponse,
InitiateKycResponseDto,
InquireApplicationResponse,
UpdateCardControlsResponseDto,
} from '../dtos/response';
import {
ICreateApplicationRequest,
IInquireApplicationRequest,
@ -17,16 +24,62 @@ import {
} from '../interfaces';
@Injectable()
export class NeoLeapService {
private readonly baseUrl: string;
private readonly logger = new Logger(NeoLeapService.name);
private readonly gatewayBaseUrl: string;
private readonly zodApiUrl: string;
private readonly apiKey: string;
private readonly useGateway: boolean;
private readonly institutionCode = '1100';
useLocalCert: boolean;
constructor(private readonly httpService: HttpService, private readonly configService: ConfigService) {
this.baseUrl = this.configService.getOrThrow<string>('GATEWAY_URL');
this.gatewayBaseUrl = this.configService.getOrThrow<string>('GATEWAY_URL');
this.apiKey = this.configService.getOrThrow<string>('GATEWAY_API_KEY');
this.useGateway = [true, 'true'].includes(this.configService.get<boolean>('USE_GATEWAY', false));
this.useLocalCert = this.configService.get<boolean>('USE_LOCAL_CERT', false);
this.zodApiUrl = this.configService.getOrThrow<string>('ZOD_API_URL');
}
async initiateKyc(customerId: string, body: InitiateKycRequestDto) {
const responseKey = 'InitiateKycResponseDetails';
if (!this.useGateway) {
const responseDto = plainToInstance(InitiateKycResponseDto, INITIATE_KYC_MOCK[responseKey], {
excludeExtraneousValues: true,
});
setTimeout(() => {
this.httpService
.post(`${this.zodApiUrl}/neoleap-webhooks/kyc`, getKycCallbackMock(body.nationalId), {
headers: {
'Content-Type': 'application/json',
},
})
.subscribe({
next: () => this.logger.log('Mock KYC webhook sent'),
error: (err) => console.error(err),
});
}, 7000);
return responseDto;
}
const payload = {
InitiateKycRequestDetails: {
CustomerIdentifier: {
InstitutionCode: this.institutionCode,
Id: customerId,
NationalId: body.nationalId,
},
},
RequestHeader: this.prepareHeaders('InitiateKyc'),
};
return this.sendRequestToNeoLeap<typeof payload, InitiateKycResponseDto>(
'kyc/InitiateKyc',
payload,
responseKey,
InitiateKycResponseDto,
);
}
async createApplication(customer: Customer) {
@ -189,7 +242,7 @@ export class NeoLeapService {
responseClass: ClassConstructor<R>,
): Promise<R> {
try {
const { data } = await this.httpService.axiosRef.post(`${this.baseUrl}/${endpoint}`, payload, {
const { data } = await this.httpService.axiosRef.post(`${this.gatewayBaseUrl}/${endpoint}`, payload, {
headers: {
'Content-Type': 'application/json',
Authorization: `${this.apiKey}`,